| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| node-npmtest-loopback/ | 100% | (153 / 153) | 100% | (126 / 126) | 100% | (28 / 28) | 100% | (153 / 153) | |
| node-npmtest-loopback/node_modules/loopback-connector-remote/ | 100% | (1 / 1) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (1 / 1) | |
| node-npmtest-loopback/node_modules/loopback-connector-remote/lib/ | 24.77% | (27 / 109) | 0% | (0 / 22) | 0% | (0 / 28) | 24.77% | (27 / 109) | |
| node-npmtest-loopback/node_modules/loopback-connector/ | 100% | (10 / 10) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (10 / 10) | |
| node-npmtest-loopback/node_modules/loopback-connector/lib/ | 19.81% | (208 / 1050) | 0.63% | (3 / 480) | 0.98% | (2 / 205) | 20% | (208 / 1040) | |
| node-npmtest-loopback/node_modules/loopback-datasource-juggler/ | 85.71% | (12 / 14) | 100% | (0 / 0) | 0% | (0 / 2) | 85.71% | (12 / 14) | |
| node-npmtest-loopback/node_modules/loopback-datasource-juggler/lib/ | 15.54% | (1101 / 7085) | 5.55% | (258 / 4649) | 4.26% | (38 / 891) | 15.96% | (1101 / 6900) | |
| node-npmtest-loopback/node_modules/loopback-datasource-juggler/lib/connectors/ | 10.16% | (57 / 561) | 0% | (0 / 360) | 0% | (0 / 94) | 10.44% | (57 / 546) | |
| node-npmtest-loopback/node_modules/loopback-datasource-juggler/lib/kvao/ | 25% | (42 / 168) | 0% | (0 / 104) | 0% | (0 / 18) | 25.77% | (42 / 163) | |
| node-npmtest-loopback/node_modules/loopback-datasource-juggler/node_modules/async/dist/ | 34.96% | (431 / 1233) | 10.49% | (64 / 610) | 7.75% | (21 / 271) | 36.1% | (430 / 1191) | |
| node-npmtest-loopback/node_modules/loopback-phase/ | 100% | (5 / 5) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (5 / 5) | |
| node-npmtest-loopback/node_modules/loopback-phase/lib/ | 22.37% | (34 / 152) | 0% | (0 / 56) | 0% | (0 / 28) | 23.29% | (34 / 146) | |
| node-npmtest-loopback/node_modules/loopback-phase/node_modules/async/lib/ | 15.59% | (97 / 622) | 3.42% | (9 / 263) | 1.97% | (4 / 203) | 15.75% | (97 / 616) | |
| node-npmtest-loopback/node_modules/loopback/ | 31.03% | (9 / 29) | 0% | (0 / 6) | 0% | (0 / 4) | 31.03% | (9 / 29) | |
| node-npmtest-loopback/node_modules/loopback/common/models/ | 15.93% | (265 / 1664) | 1% | (10 / 998) | 5.77% | (15 / 260) | 16.61% | (264 / 1589) | |
| node-npmtest-loopback/node_modules/loopback/lib/ | 24.46% | (462 / 1889) | 4.64% | (49 / 1057) | 9.49% | (28 / 295) | 25.05% | (460 / 1836) | |
| node-npmtest-loopback/node_modules/loopback/lib/connectors/ | 39.13% | (36 / 92) | 0% | (0 / 35) | 0% | (0 / 14) | 39.13% | (36 / 92) | |
| node-npmtest-loopback/node_modules/loopback/server/middleware/ | 25.84% | (23 / 89) | 0% | (0 / 48) | 0% | (0 / 16) | 26.14% | (23 / 88) |
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| example.js | 100% | (83 / 83) | 100% | (73 / 73) | 100% | (12 / 12) | 100% | (83 / 83) | |
| lib.npmtest_loopback.js | 100% | (16 / 16) | 100% | (14 / 14) | 100% | (3 / 3) | 100% | (16 / 16) | |
| test.js | 100% | (54 / 54) | 100% | (39 / 39) | 100% | (13 / 13) | 100% | (54 / 54) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | 2 2 2 2 2 2 2 1 2 2 2 2 1 2 2 2 2 2 1 2 1 1 1 1 1 1 1 1 1 2 1 1 1 1 2 2 3 3 3 3 1 3 3 3 1 3 1 1 1 1 1 1 1 1 1 1 1 1 6 6 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | /*
example.js
quickstart example
instruction
1. save this script as example.js
2. run the shell command:
$ npm install npmtest-loopback && PORT=8081 node example.js
3. play with the browser-demo on http://127.0.0.1:8081
*/
/* istanbul instrument in package npmtest_loopback */
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 96,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var local;
// run shared js-env code - pre-init
(function () {
// init local
local = {};
// init modeJs
local.modeJs = (function () {
try {
return typeof navigator.userAgent === 'string' &&
typeof document.querySelector('body') === 'object' &&
typeof XMLHttpRequest.prototype.open === 'function' &&
'browser';
} catch (errorCaughtBrowser) {
return module.exports &&
typeof process.versions.node === 'string' &&
typeof require('http').createServer === 'function' &&
'node';
}
}());
// init global
local.global = local.modeJs === 'browser'
? window
: global;
// init utility2_rollup
local = local.global.utility2_rollup || (local.modeJs === 'browser'
? local.global.utility2_npmtest_loopback
: global.utility2_moduleExports);
// export local
local.global.local = local;
}());
switch (local.modeJs) {
// post-init
// run browser js-env code - post-init
/* istanbul ignore next */
case 'browser':
local.testRunBrowser = function (event) {
Eif (!event || (event &&
event.currentTarget &&
event.currentTarget.className &&
event.currentTarget.className.includes &&
event.currentTarget.className.includes('onreset'))) {
// reset output
Array.from(
document.querySelectorAll('body > .resettable')
).forEach(function (element) {
switch (element.tagName) {
case 'INPUT':
case 'TEXTAREA':
element.value = '';
break;
default:
element.textContent = '';
}
});
}
switch (event && event.currentTarget && event.currentTarget.id) {
case 'testRunButton1':
// show tests
Eif (document.querySelector('#testReportDiv1').style.display === 'none') {
document.querySelector('#testReportDiv1').style.display = 'block';
document.querySelector('#testRunButton1').textContent =
'hide internal test';
local.modeTest = true;
local.testRunDefault(local);
// hide tests
} else {
document.querySelector('#testReportDiv1').style.display = 'none';
document.querySelector('#testRunButton1').textContent = 'run internal test';
}
break;
// custom-case
default:
break;
}
Iif (document.querySelector('#inputTextareaEval1') && (!event || (event &&
event.currentTarget &&
event.currentTarget.className &&
event.currentTarget.className.includes &&
event.currentTarget.className.includes('oneval')))) {
// try to eval input-code
try {
/*jslint evil: true*/
eval(document.querySelector('#inputTextareaEval1').value);
} catch (errorCaught) {
console.error(errorCaught);
}
}
};
// log stderr and stdout to #outputTextareaStdout1
['error', 'log'].forEach(function (key) {
console[key + '_original'] = console[key];
console[key] = function () {
var element;
console[key + '_original'].apply(console, arguments);
element = document.querySelector('#outputTextareaStdout1');
Iif (!element) {
return;
}
// append text to #outputTextareaStdout1
element.value += Array.from(arguments).map(function (arg) {
return typeof arg === 'string'
? arg
: JSON.stringify(arg, null, 4);
}).join(' ') + '\n';
// scroll textarea to bottom
element.scrollTop = element.scrollHeight;
};
});
// init event-handling
['change', 'click', 'keyup'].forEach(function (event) {
Array.from(document.querySelectorAll('.on' + event)).forEach(function (element) {
element.addEventListener(event, local.testRunBrowser);
});
});
// run tests
local.testRunBrowser();
break;
// run node js-env code - post-init
/* istanbul ignore next */
case 'node':
// export local
module.exports = local;
// require modules
local.fs = require('fs');
local.http = require('http');
local.url = require('url');
// init assets
local.assetsDict = local.assetsDict || {};
/* jslint-ignore-begin */
local.assetsDict['/assets.index.template.html'] = '\
<!doctype html>\n\
<html lang="en">\n\
<head>\n\
<meta charset="UTF-8">\n\
<meta name="viewport" content="width=device-width, initial-scale=1">\n\
<title>{{env.npm_package_name}} (v{{env.npm_package_version}})</title>\n\
<style>\n\
/*csslint\n\
box-sizing: false,\n\
universal-selector: false\n\
*/\n\
* {\n\
box-sizing: border-box;\n\
}\n\
body {\n\
background: #dde;\n\
font-family: Arial, Helvetica, sans-serif;\n\
margin: 2rem;\n\
}\n\
body > * {\n\
margin-bottom: 1rem;\n\
}\n\
.utility2FooterDiv {\n\
margin-top: 20px;\n\
text-align: center;\n\
}\n\
</style>\n\
<style>\n\
/*csslint\n\
*/\n\
textarea {\n\
font-family: monospace;\n\
height: 10rem;\n\
width: 100%;\n\
}\n\
textarea[readonly] {\n\
background: #ddd;\n\
}\n\
</style>\n\
</head>\n\
<body>\n\
<!-- utility2-comment\n\
<div id="ajaxProgressDiv1" style="background: #d00; height: 2px; left: 0; margin: 0; padding: 0; position: fixed; top: 0; transition: background 0.5s, width 1.5s; width: 25%;"></div>\n\
utility2-comment -->\n\
<h1>\n\
<!-- utility2-comment\n\
<a\n\
{{#if env.npm_package_homepage}}\n\
href="{{env.npm_package_homepage}}"\n\
{{/if env.npm_package_homepage}}\n\
target="_blank"\n\
>\n\
utility2-comment -->\n\
{{env.npm_package_name}} (v{{env.npm_package_version}})\n\
<!-- utility2-comment\n\
</a>\n\
utility2-comment -->\n\
</h1>\n\
<h3>{{env.npm_package_description}}</h3>\n\
<!-- utility2-comment\n\
<h4><a download href="assets.app.js">download standalone app</a></h4>\n\
<button class="onclick onreset" id="testRunButton1">run internal test</button><br>\n\
<div id="testReportDiv1" style="display: none;"></div>\n\
utility2-comment -->\n\
\n\
\n\
\n\
<label>stderr and stdout</label>\n\
<textarea class="resettable" id="outputTextareaStdout1" readonly></textarea>\n\
<!-- utility2-comment\n\
{{#if isRollup}}\n\
<script src="assets.app.js"></script>\n\
{{#unless isRollup}}\n\
utility2-comment -->\n\
<script src="assets.utility2.rollup.js"></script>\n\
<script src="jsonp.utility2._stateInit?callback=window.utility2._stateInit"></script>\n\
<script src="assets.npmtest_loopback.rollup.js"></script>\n\
<script src="assets.example.js"></script>\n\
<script src="assets.test.js"></script>\n\
<!-- utility2-comment\n\
{{/if isRollup}}\n\
utility2-comment -->\n\
<div class="utility2FooterDiv">\n\
[ this app was created with\n\
<a href="https://github.com/kaizhu256/node-utility2" target="_blank">utility2</a>\n\
]\n\
</div>\n\
</body>\n\
</html>\n\
';
/* jslint-ignore-end */
Iif (local.templateRender) {
local.assetsDict['/'] = local.templateRender(
local.assetsDict['/assets.index.template.html'],
{
env: local.objectSetDefault(local.env, {
npm_package_description: 'the greatest app in the world!',
npm_package_name: 'my-app',
npm_package_nameAlias: 'my_app',
npm_package_version: '0.0.1'
})
}
);
} else {
local.assetsDict['/'] = local.assetsDict['/assets.index.template.html']
.replace((/\{\{env\.(\w+?)\}\}/g), function (match0, match1) {
// jslint-hack
String(match0);
switch (match1) {
case 'npm_package_description':
return 'the greatest app in the world!';
case 'npm_package_name':
return 'my-app';
case 'npm_package_nameAlias':
return 'my_app';
case 'npm_package_version':
return '0.0.1';
}
});
}
// run the cli
Eif (local.global.utility2_rollup || module !== require.main) {
break;
}
local.assetsDict['/assets.example.js'] =
local.assetsDict['/assets.example.js'] ||
local.fs.readFileSync(__filename, 'utf8');
// bug-workaround - long $npm_package_buildCustomOrg
/* jslint-ignore-begin */
local.assetsDict['/assets.npmtest_loopback.rollup.js'] =
local.assetsDict['/assets.npmtest_loopback.rollup.js'] ||
local.fs.readFileSync(
local.npmtest_loopback.__dirname + '/lib.npmtest_loopback.js',
'utf8'
).replace((/^#!/), '//');
/* jslint-ignore-end */
local.assetsDict['/favicon.ico'] = local.assetsDict['/favicon.ico'] || '';
// if $npm_config_timeout_exit exists,
// then exit this process after $npm_config_timeout_exit ms
if (Number(process.env.npm_config_timeout_exit)) {
setTimeout(process.exit, Number(process.env.npm_config_timeout_exit));
}
// start server
if (local.global.utility2_serverHttp1) {
break;
}
process.env.PORT = process.env.PORT || '8081';
console.error('server starting on port ' + process.env.PORT);
local.http.createServer(function (request, response) {
request.urlParsed = local.url.parse(request.url);
if (local.assetsDict[request.urlParsed.pathname] !== undefined) {
response.end(local.assetsDict[request.urlParsed.pathname]);
return;
}
response.statusCode = 404;
response.end();
}).listen(process.env.PORT);
break;
}
}());
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 2 2 2 2 2 2 2 1 2 2 2 2 1 1 1 1 | /* istanbul instrument in package npmtest_loopback */
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 96,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var local;
// run shared js-env code - pre-init
(function () {
// init local
local = {};
// init modeJs
local.modeJs = (function () {
try {
return typeof navigator.userAgent === 'string' &&
typeof document.querySelector('body') === 'object' &&
typeof XMLHttpRequest.prototype.open === 'function' &&
'browser';
} catch (errorCaughtBrowser) {
return module.exports &&
typeof process.versions.node === 'string' &&
typeof require('http').createServer === 'function' &&
'node';
}
}());
// init global
local.global = local.modeJs === 'browser'
? window
: global;
// init utility2_rollup
local = local.global.utility2_rollup || local;
// init lib
local.local = local.npmtest_loopback = local;
// init exports
if (local.modeJs === 'browser') {
local.global.utility2_npmtest_loopback = local;
} else {
module.exports = local;
module.exports.__dirname = __dirname;
module.exports.module = module;
}
}());
}());
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | 2 2 2 2 2 2 2 1 2 2 1 1 1 1 2 2 2 2 1 1 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 2 2 1 2 2 1 2 2 1 1 1 1 1 | /* istanbul instrument in package npmtest_loopback */
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 96,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var local;
// run shared js-env code - pre-init
(function () {
// init local
local = {};
// init modeJs
local.modeJs = (function () {
try {
return typeof navigator.userAgent === 'string' &&
typeof document.querySelector('body') === 'object' &&
typeof XMLHttpRequest.prototype.open === 'function' &&
'browser';
} catch (errorCaughtBrowser) {
return module.exports &&
typeof process.versions.node === 'string' &&
typeof require('http').createServer === 'function' &&
'node';
}
}());
// init global
local.global = local.modeJs === 'browser'
? window
: global;
switch (local.modeJs) {
// re-init local from window.local
case 'browser':
local = local.global.utility2.objectSetDefault(
local.global.utility2_rollup || local.global.local,
local.global.utility2
);
break;
// re-init local from example.js
case 'node':
local = (local.global.utility2_rollup || require('utility2'))
.requireExampleJsFromReadme();
break;
}
// export local
local.global.local = local;
}());
// run shared js-env code - function
(function () {
return;
}());
switch (local.modeJs) {
// run browser js-env code - function
case 'browser':
break;
// run node js-env code - function
case 'node':
break;
}
// run shared js-env code - post-init
(function () {
return;
}());
switch (local.modeJs) {
// run browser js-env code - post-init
case 'browser':
local.testCase_browser_nullCase = local.testCase_browser_nullCase || function (
options,
onError
) {
/*
* this function will test browsers's null-case handling-behavior-behavior
*/
onError(null, options);
};
// run tests
local.nop(local.modeTest &&
document.querySelector('#testRunButton1') &&
document.querySelector('#testRunButton1').click());
break;
// run node js-env code - post-init
/* istanbul ignore next */
case 'node':
local.testCase_buildApidoc_default = local.testCase_buildApidoc_default || function (
options,
onError
) {
/*
* this function will test buildApidoc's default handling-behavior-behavior
*/
options = { modulePathList: module.paths };
local.buildApidoc(options, onError);
};
local.testCase_buildApp_default = local.testCase_buildApp_default || function (
options,
onError
) {
/*
* this function will test buildApp's default handling-behavior-behavior
*/
local.testCase_buildReadme_default(options, local.onErrorThrow);
local.testCase_buildLib_default(options, local.onErrorThrow);
local.testCase_buildTest_default(options, local.onErrorThrow);
local.testCase_buildCustomOrg_default(options, local.onErrorThrow);
options = [];
local.buildApp(options, onError);
};
local.testCase_buildCustomOrg_default = local.testCase_buildCustomOrg_default ||
function (options, onError) {
/*
* this function will test buildCustomOrg's default handling-behavior
*/
options = {};
local.buildCustomOrg(options, onError);
};
local.testCase_buildLib_default = local.testCase_buildLib_default || function (
options,
onError
) {
/*
* this function will test buildLib's default handling-behavior
*/
options = {};
local.buildLib(options, onError);
};
local.testCase_buildReadme_default = local.testCase_buildReadme_default || function (
options,
onError
) {
/*
* this function will test buildReadme's default handling-behavior-behavior
*/
options = {};
local.buildReadme(options, onError);
};
local.testCase_buildTest_default = local.testCase_buildTest_default || function (
options,
onError
) {
/*
* this function will test buildTest's default handling-behavior
*/
options = {};
local.buildTest(options, onError);
};
local.testCase_webpage_default = local.testCase_webpage_default || function (
options,
onError
) {
/*
* this function will test webpage's default handling-behavior
*/
options = { modeCoverageMerge: true, url: local.serverLocalHost + '?modeTest=1' };
local.browserTest(options, onError);
};
// run test-server
local.testRunServer(local);
break;
}
}());
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| index.js | 100% | (1 / 1) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (1 / 1) |
| 1 2 3 4 5 6 7 | 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback-connector-remote
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
module.exports = require('./lib/remote-connector');
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| relations.js | 25% | (12 / 48) | 100% | (0 / 0) | 0% | (0 / 16) | 25% | (12 / 48) | |
| remote-connector.js | 24.59% | (15 / 61) | 0% | (0 / 22) | 0% | (0 / 12) | 24.59% | (15 / 61) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector-remote
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/*!
* Dependencies
*/
var relation = require('loopback-datasource-juggler/lib/relation-definition');
var RelationDefinition = relation.RelationDefinition;
module.exports = RelationMixin;
/**
* RelationMixin class. Use to define relationships between models.
*
* @class RelationMixin
*/
function RelationMixin() {
}
/**
* Define a "one to many" relationship by specifying the model name
*
* Examples:
* ```
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
* ```
*
* ```
* Book.hasMany(Chapter);
* ```
* Or, equivalently:
* ```
* Book.hasMany('chapters', {model: Chapter});
* ```
*
* Query and create related models:
*
* ```js
* Book.create(function(err, book) {
*
* // Create a chapter instance ready to be saved in the data source.
* var chapter = book.chapters.build({name: 'Chapter 1'});
*
* // Save the new chapter
* chapter.save();
*
* // you can also call the Chapter.create method with the `chapters` property
* // which will build a chapter instance and save the it in the data source.
* book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
* // this callback is optional
* });
*
* // Query chapters for the book
* book.chapters(function(err, chapters) {
* // all chapters with bookId = book.id
* console.log(chapters);
* });
*
* book.chapters({where: {name: 'test'}, function(err, chapters) {
* // All chapters with bookId = book.id and name = 'test'
* console.log(chapters);
* });
* });
*```
* @param {Object|String} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} parameters Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationMixin.hasMany = function hasMany(modelTo, params) {
var def = RelationDefinition.hasMany(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
/**
* Declare "belongsTo" relation that sets up a one-to-one connection with
* another model, such that each instance of the declaring model "belongs
* to" one instance of the other model.
*
* For example, if an application includes users and posts, and each post can be
* written by exactly one user. The following code specifies that `Post` has a
* reference called `author` to the `User` model via the `userId` property of
* `Post` as the foreign key.
* ```
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
* ```
* You can then access the author in one of the following styles.
* Get the User object for the post author asynchronously:
* ```
* post.author(callback);
* ```
* Get the User object for the post author synchronously:
* ```
* post.author();
* Set the author to be the given user:
* ```
* post.author(user)
* ```
* Examples:
*
* Suppose the model Post has a *belongsTo* relationship with User (the author
* of the post). You could declare it this way:
* ```js
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
* ```
*
* When a post is loaded, you can load the related author with:
* ```js
* post.author(function(err, user) {
* // the user variable is your user object
* });
* ```
*
* The related object is cached, so if later you try to get again the author, no
* additional request will be made. But there is an optional boolean parameter
* in first position that set whether or not you want to reload the cache:
* ```js
* post.author(true, function(err, user) {
* // The user is reloaded, even if it was already cached.
* });
* ```
* This optional parameter default value is false, so the related object will
* be loaded from cache if available.
*
* @param {Class|String} modelTo Model object (or String name of model)
* to which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Name of foreign key property.
*
*/
RelationMixin.belongsTo = function(modelTo, params) {
var def = RelationDefinition.belongsTo(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
/**
* A hasAndBelongsToMany relation creates a direct many-to-many connection with
* another model, with no intervening model. For example, if your application
* includes users and groups, with each group having many users and each user
* appearing in many groups, you could declare the models this way:
* ```
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
* ```
* Then, to get the groups to which the user belongs:
* ```
* user.groups(callback);
* ```
* Create a new group and connect it with the user:
* ```
* user.groups.create(data, callback);
* ```
* Connect an existing group with the user:
* ```
* user.groups.add(group, callback);
* ```
* Remove the user from the group:
* ```
* user.groups.remove(group, callback);
* ```
*
* @param {String|Object} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationMixin.hasAndBelongsToMany =
function hasAndBelongsToMany(modelTo, params) {
var def = RelationDefinition.hasAndBelongsToMany(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
RelationMixin.hasOne = function hasOne(modelTo, params) {
var def = RelationDefinition.hasOne(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
RelationMixin.referencesMany = function referencesMany(modelTo, params) {
var def = RelationDefinition.referencesMany(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
RelationMixin.embedsOne = function embedsOne(modelTo, params) {
var def = RelationDefinition.embedsOne(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
RelationMixin.embedsMany = function embedsMany(modelTo, params) {
var def = RelationDefinition.embedsMany(this, modelTo, params);
this.dataSource.adapter.resolve(this);
defineRelationProperty(this, def);
};
function defineRelationProperty(modelClass, def) {
Object.defineProperty(modelClass.prototype, def.name, {
get: function() {
var that = this;
var scope = function() {
return that['__get__' + def.name].apply(that, arguments);
};
scope.count = function() {
return that['__count__' + def.name].apply(that, arguments);
};
scope.create = function() {
return that['__create__' + def.name].apply(that, arguments);
};
scope.deleteById = scope.destroyById = function() {
return that['__destroyById__' + def.name].apply(that, arguments);
};
scope.exists = function() {
return that['__exists__' + def.name].apply(that, arguments);
};
scope.findById = function() {
return that['__findById__' + def.name].apply(that, arguments);
};
return scope;
}
});
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector-remote
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/**
* Dependencies.
*/
var assert = require('assert');
var remoting = require('strong-remoting');
var utils = require('loopback-datasource-juggler/lib/utils');
var jutil = require('loopback-datasource-juggler/lib/jutil');
var RelationMixin = require('./relations');
var InclusionMixin = require('loopback-datasource-juggler/lib/include');
/**
* Export the RemoteConnector class.
*/
module.exports = RemoteConnector;
/**
* Create an instance of the connector with the given `settings`.
*/
function RemoteConnector(settings) {
assert(typeof settings ===
'object',
'cannot initiaze RemoteConnector without a settings object');
this.client = settings.client;
this.adapter = settings.adapter || 'rest';
this.protocol = settings.protocol || 'http';
this.root = settings.root || '';
this.host = settings.host || 'localhost';
this.port = settings.port || 3000;
this.remotes = remoting.create();
this.name = 'remote-connector';
if (settings.url) {
this.url = settings.url;
} else {
this.url = this.protocol + '://' + this.host + ':' + this.port + this.root;
}
// handle mixins in the define() method
var DAO = this.DataAccessObject = function() {
};
}
RemoteConnector.prototype.connect = function() {
this.remotes.connect(this.url, this.adapter);
};
RemoteConnector.initialize = function(dataSource, callback) {
var connector = dataSource.connector =
new RemoteConnector(dataSource.settings);
connector.connect();
process.nextTick(callback);
};
RemoteConnector.prototype.define = function(definition) {
var Model = definition.model;
var remotes = this.remotes;
assert(Model.sharedClass,
'cannot attach ' +
Model.modelName +
' to a remote connector without a Model.sharedClass');
jutil.mixin(Model, RelationMixin);
jutil.mixin(Model, InclusionMixin);
remotes.addClass(Model.sharedClass);
this.resolve(Model);
};
RemoteConnector.prototype.resolve = function(Model) {
var remotes = this.remotes;
Model.sharedClass.methods().forEach(function(remoteMethod) {
if (remoteMethod.name !== 'Change' && remoteMethod.name !== 'Checkpoint') {
createProxyMethod(Model, remotes, remoteMethod);
}
});
// setup a remoting type converter for this model
remotes.defineObjectType(Model.modelName, function(data) {
return new Model(data);
});
};
function createProxyMethod(Model, remotes, remoteMethod) {
var scope = remoteMethod.isStatic ? Model : Model.prototype;
var original = scope[remoteMethod.name];
function remoteMethodProxy() {
var args = Array.prototype.slice.call(arguments);
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
var callback;
if (lastArgIsFunc) {
callback = args.pop();
} else {
callback = utils.createPromiseCallback();
}
if (remoteMethod.isStatic) {
remotes.invoke(remoteMethod.stringName, args, callback);
} else {
var ctorArgs = [this.id];
remotes.invoke(remoteMethod.stringName, ctorArgs, args, callback);
}
return callback.promise;
}
scope[remoteMethod.name] = remoteMethodProxy;
remoteMethod.aliases.forEach(function(alias) {
scope[alias] = remoteMethodProxy;
});
}
function noop() {
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| index.js | 100% | (10 / 10) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (10 / 10) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var SG = require('strong-globalize');
SG.SetRootDir(__dirname);
exports.Connector = require('./lib/connector');
// Set up SqlConnector as an alias to SQLConnector
exports.SQLConnector = exports.SqlConnector = require('./lib/sql');
exports.ParameterizedSQL = exports.SQLConnector.ParameterizedSQL;
exports.Transaction = require('./lib/transaction');
exports.createPromiseCallback = require('./lib/utils').createPromiseCallback;
// KeyValue helpers
exports.ModelKeyComposer = require('./lib/model-key-composer');
exports.BinaryPacker = require('./lib/binary-packer');
exports.JSONStringPacker = require('./lib/json-string-packer');
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| binary-packer.js | 28.57% | (8 / 28) | 0% | (0 / 4) | 0% | (0 / 9) | 28.57% | (8 / 28) | |
| connector.js | 41.77% | (33 / 79) | 6% | (3 / 50) | 8.33% | (2 / 24) | 41.77% | (33 / 79) | |
| json-string-packer.js | 18.18% | (6 / 33) | 0% | (0 / 15) | 0% | (0 / 9) | 18.18% | (6 / 33) | |
| model-key-composer.js | 26.09% | (6 / 23) | 0% | (0 / 6) | 0% | (0 / 5) | 26.09% | (6 / 23) | |
| parameterized-sql.js | 23.08% | (9 / 39) | 0% | (0 / 24) | 0% | (0 / 5) | 23.08% | (9 / 39) | |
| sql.js | 15.98% | (128 / 801) | 0% | (0 / 363) | 0% | (0 / 145) | 16.16% | (128 / 792) | |
| transaction.js | 41.67% | (15 / 36) | 0% | (0 / 16) | 0% | (0 / 5) | 41.67% | (15 / 36) | |
| utils.js | 27.27% | (3 / 11) | 0% | (0 / 2) | 0% | (0 / 3) | 30% | (3 / 10) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var createPromiseCallback = require('./utils').createPromiseCallback;
var msgpack = require('msgpack5');
module.exports = BinaryPacker;
/**
* Create a new Packer instance that can be used to convert between JavaScript
* objects and a binary representation in a Buffer.
*
* Compared to JSON, this encoding preserves the following JavaScript types:
* - Date
*/
function BinaryPacker() {
this._packer = msgpack({forceFloat64: true});
this._packer.register(1, Date, encodeDate, decodeDate);
}
/**
* Encode the provided value to a `Buffer`.
*
* @param {*} value Any value (string, number, object)
* @callback {Function} cb The callback to receive the parsed result.
* @param {Error} err
* @param {Buffer} data The encoded value
* @promise
*/
BinaryPacker.prototype.encode = function(value, cb) {
cb = cb || createPromiseCallback();
try {
// msgpack5 returns https://www.npmjs.com/package/bl instead of Buffer
// use .slice() to convert to a Buffer
var data = this._packer.encode(value).slice();
setImmediate(function() {
cb(null, data);
});
} catch (err) {
setImmediate(function() {
cb(err);
});
}
return cb.promise;
};
/**
* Decode the binary value back to a JavaScript value.
* @param {Buffer} binary The binary input.
* @callback {Function} cb The callback to receive the composed value.
* @param {Error} err
* @param {*} value Decoded value.
* @promise
*/
BinaryPacker.prototype.decode = function(binary, cb) {
cb = cb || createPromiseCallback();
try {
var value = this._packer.decode(binary);
setImmediate(function() {
cb(null, value);
});
} catch (err) {
setImmediate(function() {
cb(err);
});
}
return cb.promise;
};
function encodeDate(obj) {
return new Buffer(obj.toISOString(), 'utf8');
}
function decodeDate(buf) {
return new Date(buf.toString('utf8'));
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 7 7 10 10 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var SG = require('strong-globalize');
var g = SG();
var debug = require('debug')('loopback:connector');
module.exports = Connector;
/**
* Base class for LoopBack connector. This is more a collection of useful
* methods for connectors than a super class
* @constructor
*/
function Connector(name, settings) {
this._models = {};
this.name = name;
this.settings = settings || {};
}
/**
* Set the relational property to indicate the backend is a relational DB
* @type {boolean}
*/
Connector.prototype.relational = false;
/**
* Check if the connector is for a relational DB
* @returns {Boolean} true for relational DB
*/
Connector.prototype.isRelational = function() {
return this.isRelational ||
(this.getTypes().indexOf('rdbms') !== -1);
};
/**
* Get types associated with the connector
* @returns {String[]} The types for the connector
*/
Connector.prototype.getTypes = function() {
return ['db', 'nosql'];
};
/**
* Get the default data type for ID
* @param prop Property definition
* @returns {Function} The default type for ID
*/
Connector.prototype.getDefaultIdType = function(prop) {
/* jshint unused:false */
return String;
};
/**
* Generate random id. Each data source model must override this method.
* @param {String} modelName Model name
* @returns {<model dependent>} Data type varies from model to model,
*/
Connector.prototype.generateUniqueId = function(modelName) {
var idType = this.getDefaultIdType && this.getDefaultIdType();
var isTypeFunction = (typeof idType === 'function');
var id = this.generateValueByColumnType ? this.generateValueByColumnType(idType) :
(typeof idType === 'function' ? idType() : null);
return id;
};
/**
* Get the metadata for the connector
* @returns {Object} The metadata object
* @property {String} type The type for the backend
* @property {Function} defaultIdType The default id type
* @property {Boolean} [isRelational] If the connector represents a relational
* database
* @property {Object} schemaForSettings The schema for settings object
*/
Connector.prototype.getMetadata = function() {
if (!this._metadata) {
this._metadata = {
types: this.getTypes(),
defaultIdType: this.getDefaultIdType(),
isRelational: this.isRelational(),
schemaForSettings: {},
};
}
return this._metadata;
};
/**
* Execute a command with given parameters
* @param {String|Object} command The command such as SQL
* @param {*[]} [params] An array of parameter values
* @param {Object} [options] Options object
* @param {Function} [callback] The callback function
*/
Connector.prototype.execute = function(command, params, options, callback) {
throw new Error(g.f('execute() must be implemented by the connector'));
};
/**
* Get the model definition by name
* @param {String} modelName The model name
* @returns {ModelDefinition} The model definition
*/
Connector.prototype.getModelDefinition = function(modelName) {
return this._models[modelName];
};
/**
* Get connector specific settings for a given model, for example,
* ```
* {
* "postgresql": {
* "schema": "xyz"
* }
* }
* ```
*
* @param {String} modelName Model name
* @returns {Object} The connector specific settings
*/
Connector.prototype.getConnectorSpecificSettings = function(modelName) {
var settings = this.getModelDefinition(modelName).settings || {};
return settings[this.name];
};
/**
* Get model property definition
* @param {String} modelName Model name
* @param {String} propName Property name
* @returns {Object} Property definition
*/
Connector.prototype.getPropertyDefinition = function(modelName, propName) {
var model = this.getModelDefinition(modelName);
return model && model.properties[propName];
};
/**
* Look up the data source by model name
* @param {String} model The model name
* @returns {DataSource} The data source
*/
Connector.prototype.getDataSource = function(model) {
var m = this.getModelDefinition(model);
if (!m) {
debug('Model not found: ' + model);
}
return m && m.model.dataSource;
};
/**
* Get the id property name
* @param {String} model The model name
* @returns {String} The id property name
*/
Connector.prototype.idName = function(model) {
return this.getDataSource(model).idName(model);
};
/**
* Get the id property names
* @param {String} model The model name
* @returns {[String]} The id property names
*/
Connector.prototype.idNames = function(model) {
return this.getDataSource(model).idNames(model);
};
/**
* Get the id index (sequence number, starting from 1)
* @param {String} model The model name
* @param {String} prop The property name
* @returns {Number} The id index, undefined if the property is not part
* of the primary key
*/
Connector.prototype.id = function(model, prop) {
var p = this.getModelDefinition(model).properties[prop];
return p && p.id;
};
/**
* Hook to be called by DataSource for defining a model
* @param {Object} modelDefinition The model definition
*/
Connector.prototype.define = function(modelDefinition) {
modelDefinition.settings = modelDefinition.settings || {};
this._models[modelDefinition.model.modelName] = modelDefinition;
};
/**
* Hook to be called by DataSource for defining a model property
* @param {String} model The model name
* @param {String} propertyName The property name
* @param {Object} propertyDefinition The object for property definition
*/
Connector.prototype.defineProperty = function(model, propertyName, propertyDefinition) {
var modelDef = this.getModelDefinition(model);
modelDef.properties[propertyName] = propertyDefinition;
};
/**
* Disconnect from the connector
* @param {Function} [cb] Callback function
*/
Connector.prototype.disconnect = function disconnect(cb) {
// NO-OP
if (cb) {
process.nextTick(cb);
}
};
/**
* Get the id value for the given model
* @param {String} model The model name
* @param {Object} data The model instance data
* @returns {*} The id value
*
*/
Connector.prototype.getIdValue = function(model, data) {
return data && data[this.idName(model)];
};
/**
* Set the id value for the given model
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {*} value The id value
*
*/
Connector.prototype.setIdValue = function(model, data, value) {
if (data) {
data[this.idName(model)] = value;
}
};
/**
* Test if a property is nullable
* @param {Object} prop The property definition
* @returns {boolean} true if nullable
*/
Connector.prototype.isNullable = function(prop) {
if (prop.required || prop.id) {
return false;
}
if (prop.nullable || prop['null'] || prop.allowNull) {
return true;
}
if (prop.nullable === false || prop['null'] === false ||
prop.allowNull === false) {
return false;
}
return true;
};
/**
* Return the DataAccessObject interface implemented by the connector
* @returns {Object} An object containing all methods implemented by the
* connector that can be mixed into the model class. It should be considered as
* the interface.
*/
Connector.prototype.getDataAccessObject = function() {
return this.DataAccessObject;
};
/*!
* Define aliases to a prototype method/property
* @param {Function} cls The class that owns the method/property
* @param {String} methodOrPropertyName The official property method/property name
* @param {String|String[]} aliases Aliases to the official property/method
*/
Connector.defineAliases = function(cls, methodOrPropertyName, aliases) {
Iif (typeof aliases === 'string') {
aliases = [aliases];
}
Eif (Array.isArray(aliases)) {
aliases.forEach(function(alias) {
Eif (typeof alias === 'string') {
Object.defineProperty(cls, alias, {
get: function() {
return this[methodOrPropertyName];
},
});
}
});
}
};
/**
* `command()` and `query()` are aliases to `execute()`
*/
Connector.defineAliases(Connector.prototype, 'execute', ['command', 'query']);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var createPromiseCallback = require('./utils').createPromiseCallback;
module.exports = JSONStringPacker;
var ISO_DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
/**
* Create a new Packer instance that can be used to convert between JavaScript
* objects and a JsonString representation in a String.
*
* @param {String} encoding Buffer encoding refer to https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings
*/
function JSONStringPacker(encoding) {
this.encoding = encoding || 'base64';
}
/**
* Encode the provided value to a `JsonString`.
*
* @param {*} value Any value (string, number, object)
* @callback {Function} cb The callback to receive the parsed result.
* @param {Error} err
* @param {Buffer} data The encoded value
* @promise
*/
JSONStringPacker.prototype.encode = function(value, cb) {
var encoding = this.encoding;
cb = cb || createPromiseCallback();
try {
var data = JSON.stringify(value, function(key, value) {
if (Buffer.isBuffer(this[key])) {
return {
type: 'Buffer',
data: this[key].toString(encoding),
};
} else {
return value;
}
});
setImmediate(function() {
cb(null, data);
});
} catch (err) {
setImmediate(function() {
cb(err);
});
}
return cb.promise;
};
/**
* Decode the JsonString value back to a JavaScript value.
* @param {String} jsonString The JsonString input.
* @callback {Function} cb The callback to receive the composed value.
* @param {Error} err
* @param {*} value Decoded value.
* @promise
*/
JSONStringPacker.prototype.decode = function(jsonString, cb) {
var encoding = this.encoding;
cb = cb || createPromiseCallback();
try {
var value = JSON.parse(jsonString, function(k, v) {
if (v && v.type && v.type === 'Buffer') {
return new Buffer(v.data, encoding);
}
if (ISO_DATE_REGEXP.exec(v)) {
return new Date(v);
}
return v;
});
setImmediate(function() {
cb(null, value);
});
} catch (err) {
setImmediate(function() {
cb(err);
});
}
return cb.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 1 1 1 1 1 1 | // Copyright IBM Corp. 2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var createPromiseCallback = require('./utils').createPromiseCallback;
var debug = require('debug')('loopback:connector:model-key-composer');
var g = require('strong-globalize')();
/**
* Build a single key string from a tuple (modelName, key).
*
* This method is typically used by KeyValue connectors to build a single
* key string for a given modelName+key tuple.
*
* @param {String} modelName
* @param {String} key
* @callback {Function} cb The callback to receive the composed value.
* @param {Error} err
* @param {String} composedKey
* @promise
*/
exports.compose = function composeKeyFromModelNameAndKey(modelName, key, cb) {
cb = cb || createPromiseCallback();
// Escape model name to prevent collision
// 'model' + 'foo:bar' --vs-- 'model:foo' + 'bar'
var value = encodeURIComponent(modelName) + ':' + key;
setImmediate(function() {
cb(null, value);
});
return cb.promise;
};
var PARSE_KEY_REGEX = /^([^:]*):(.*)/;
/**
* Parse a composed key string into a tuple (modelName, key).
*
* This method is typically used by KeyValue connectors to parse a composed
* key string returned by SCAN/ITERATE method back to the expected
* modelName+tuple key.
*
* @param {String} composed The composed key as returned by `composeKey`
* @callback {Function} cb The callback to receive the parsed result.
* @param {Error} err
* @param {Object} result The result with properties `modelName` and `key`.
* @promise
*/
exports.parse = function(composed, cb) {
cb = cb || createPromiseCallback();
var matchResult = composed.match(PARSE_KEY_REGEX);
if (matchResult) {
var result = {
modelName: matchResult[1],
key: matchResult[2],
};
setImmediate(function() {
cb(null, result);
});
} else {
debug('Invalid key - missing model-name prefix: %s', composed);
var err = new Error(g.f(
'Invalid key %j - missing model-name prefix',
composed));
err.code = 'NO_MODEL_PREFIX';
setImmediate(function() {
cb(err);
});
}
return cb.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var PLACEHOLDER = '?';
module.exports = ParameterizedSQL;
/**
* A class for parameterized SQL clauses
* @param {String|Object} sql The SQL clause. If the value is a string, treat
* it as the template using `?` as the placeholder, for example, `(?,?)`. If
* the value is an object, treat it as {sql: '...', params: [...]}
* @param {*[]} params An array of parameter values. The length should match the
* number of placeholders in the template
* @returns {ParameterizedSQL} A new instance of ParameterizedSQL
* @constructor
*/
function ParameterizedSQL(sql, params) {
if (!(this instanceof ParameterizedSQL)) {
return new ParameterizedSQL(sql, params);
}
sql = sql || '';
if (arguments.length === 1 && typeof sql === 'object') {
this.sql = sql.sql;
this.params = sql.params || [];
} else {
this.sql = sql;
this.params = params || [];
}
assert(typeof this.sql === 'string', 'sql must be a string');
assert(Array.isArray(this.params), 'params must be an array');
var parts = this.sql.split(PLACEHOLDER);
assert(parts.length - 1 === this.params.length,
'The number of ? (' + (parts.length - 1) +
') in the sql (' + this.sql + ') must match the number of params (' +
this.params.length +
') ' + this.params);
}
/**
* Merge the parameterized sqls into the current instance
* @param {Object|Object[]} ps A parametered SQL or an array of parameterized
* SQLs
* @param {String} [separator] Separator, default to ` `
* @returns {ParameterizedSQL} The current instance
*/
ParameterizedSQL.prototype.merge = function(ps, separator) {
if (Array.isArray(ps)) {
return this.constructor.append(this,
this.constructor.join(ps, separator), separator);
} else {
return this.constructor.append(this, ps, separator);
}
};
ParameterizedSQL.prototype.toJSON = function() {
return {
sql: this.sql,
params: this.params,
};
};
/**
* Append the statement into the current statement
* @param {Object} currentStmt The current SQL statement
* @param {Object} stmt The statement to be appended
* @param {String} [separator] Separator, default to ` `
* @returns {*} The merged statement
*/
ParameterizedSQL.append = function(currentStmt, stmt, separator) {
currentStmt = (currentStmt instanceof ParameterizedSQL) ?
currentStmt : new ParameterizedSQL(currentStmt);
stmt = (stmt instanceof ParameterizedSQL) ? stmt :
new ParameterizedSQL(stmt);
separator = typeof separator === 'string' ? separator : ' ';
if (currentStmt.sql) {
currentStmt.sql += separator;
}
if (stmt.sql) {
currentStmt.sql += stmt.sql;
}
currentStmt.params = currentStmt.params.concat(stmt.params);
return currentStmt;
};
/**
* Join multiple parameterized SQLs into one
* @param {Object[]} sqls An array of parameterized SQLs
* @param {String} [separator] Separator, default to ` `
* @returns {ParameterizedSQL}
*/
ParameterizedSQL.join = function(sqls, separator) {
assert(Array.isArray(sqls), 'sqls must be an array');
var ps = new ParameterizedSQL('', []);
for (var i = 0, n = sqls.length; i < n; i++) {
this.append(ps, sqls[i], separator);
}
return ps;
};
ParameterizedSQL.PLACEHOLDER = PLACEHOLDER;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var SG = require('strong-globalize');
var g = SG();
var util = require('util');
var async = require('async');
var assert = require('assert');
var Connector = require('./connector');
var debug = require('debug')('loopback:connector:sql');
var ParameterizedSQL = require('./parameterized-sql');
var Transaction = require('./transaction');
module.exports = SQLConnector;
function NOOP() {}
/**
* Base class for connectors that connect to relational databases using SQL
* @class
*/
function SQLConnector() {
// Call the super constructor
Connector.apply(this, [].slice.call(arguments));
}
// Inherit from the base Connector
util.inherits(SQLConnector, Connector);
// Export ParameterizedSQL
SQLConnector.ParameterizedSQL = ParameterizedSQL;
// The generic placeholder
var PLACEHOLDER = SQLConnector.PLACEHOLDER = ParameterizedSQL.PLACEHOLDER;
SQLConnector.Transaction = Transaction;
/**
* Set the relational property to indicate the backend is a relational DB
* @type {boolean}
*/
SQLConnector.prototype.relational = true;
/**
* Invoke a prototype method on the super class
* @param {String} methodName Method name
*/
SQLConnector.prototype.invokeSuper = function(methodName) {
var args = [].slice.call(arguments, 1);
var superMethod = this.constructor.super_.prototype[methodName];
return superMethod.apply(this, args);
};
/**
* Perform autoupdate for the given models
* @param {String[]} [models] A model name or an array of model names.
* If not present, apply to all models
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.autoupdate = function(models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(this._models);
async.each(models, function(model, done) {
if (!(model in self._models)) {
return process.nextTick(function() {
done(new Error(g.f('Model not found: %s', model)));
});
}
self.getTableStatus(model, function(err, fields, indexes, FKs) {
if (!err && fields.length) {
self.alterTable(model, fields, indexes, done);
} else {
self.createTable(model, done);
}
});
}, cb);
};
/**
* Check if the models exist
* @param {String[]} [models] A model name or an array of model names.
* If not present, apply to all models
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.isActual = function(models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(this._models);
var changes = [];
async.each(models, function(model, done) {
self.getTableStatus(model, function(err, fields) {
changes = changes.concat(self.getAddModifyColumns(model, fields));
changes = changes.concat(self.getDropColumns(model, fields));
done(err);
});
}, function done(err) {
if (err) {
return cb && cb(err);
}
var actual = (changes.length === 0);
if (cb) cb(null, actual);
});
};
SQLConnector.prototype.getAddModifyColumns = function(model, fields) {
var sql = [];
var self = this;
sql = sql.concat(self.getColumnsToAdd(model, fields));
return sql;
};
SQLConnector.prototype.getColumnsToAdd = function(model, fields) {
throw new Error(g.f('{{getColumnsToAdd()}} must be implemented by the connector'));
};
SQLConnector.prototype.getDropColumns = function(model, fields) {
var sql = [];
var self = this;
sql = sql.concat(self.getColumnsToDrop(model, fields));
return sql;
};
SQLConnector.prototype.getColumnsToDrop = function(model, fields) {
throw new Error(g.f('{{getColumnsToDrop()}} must be implemented by the connector'));
};
SQLConnector.prototype.searchForPropertyInActual = function(model, propName,
actualFields) {
var self = this;
var found = false;
actualFields.forEach(function(f) {
if (f.column === self.column(model, propName)) {
found = f;
return;
}
});
return found;
};
SQLConnector.prototype.addPropertyToActual = function(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName) +
' ' + self.columnDataType(model, propName) +
(self.isNullable(self.getPropertyDefinition(model, propName)) ?
'' : ' NOT NULL');
return sqlCommand;
};
SQLConnector.prototype.columnDataType = function(model, property) {
var columnMetadata = this.columnMetadata(model, property);
var colType = columnMetadata && columnMetadata.dataType;
if (colType) {
colType = colType.toUpperCase();
}
var prop = this.getModelDefinition(model).properties[property];
if (!prop) {
return null;
}
var colLength = columnMetadata && columnMetadata.dataLength ||
prop.length || prop.limit;
if (colType && colLength) {
return colType + '(' + colLength + ')';
}
return this.buildColumnType(prop);
};
SQLConnector.prototype.buildColumnType = function(property) {
throw new Error(g.f('{{buildColumnType()}} must be implemented by the connector'));
};
SQLConnector.prototype.propertyHasNotBeenDeleted = function(model, propName) {
return !!this.getModelDefinition(model).properties[propName];
};
SQLConnector.prototype.applySqlChanges = function(model, pendingChanges, cb) {
var self = this;
if (pendingChanges.length) {
var thisQuery = 'ALTER TABLE ' + self.tableEscaped(model);
var ranOnce = false;
pendingChanges.forEach(function(change) {
if (ranOnce) {
thisQuery = thisQuery + ' ';
}
thisQuery = thisQuery + ' ' + change;
ranOnce = true;
});
self.execute(thisQuery, cb);
}
};
/**
* Alters a table
* @param {String} model The model name
* @param {Object} fields Fields of the table
* @param {Object} indexes Indexes of the table
* @param {Function} cb The callback function
*/
SQLConnector.prototype.alterTable = function(model, fields, indexes, cb) {
throw new Error(g.f('{{alterTable()}} must be implemented by the connector'));
};
SQLConnector.prototype.checkFieldAndIndex = function(fields, indexes) {
return true;
};
/**
* Get the status of a table
* @param {String} model The model name
* @param {Function} cb The callback function
*/
SQLConnector.prototype.getTableStatus = function(model, cb) {
var fields, indexes;
var self = this;
this.showFields(model, function(err, data) {
if (err) return cb(err);
fields = data;
self.showIndexes(model, function(err, data) {
if (err) return cb(err);
indexes = data;
if (self.checkFieldAndIndex(fields, indexes))
return cb(null, fields, indexes);
});
});
};
/**
* Get fields from a table
* @param {String} model The model name
* @param {Function} cb The callback function
*/
SQLConnector.prototype.showFields = function(model, cb) {
throw new Error(g.f('{{showFields()}} must be implemented by the connector'));
};
/**
* Get indexes from a table
* @param {String} model The model name
* @param {Function} cb The callback function
*/
SQLConnector.prototype.showIndexes = function(model, cb) {
throw new Error(g.f('{{showIndexes()}} must be implemented by the connector'));
};
/**
* Get types associated with the connector
* Returns {String[]} The types for the connector
*/
SQLConnector.prototype.getTypes = function() {
return ['db', 'rdbms', 'sql'];
};
/**
* Get the default data type for ID
* @param prop Property definition
* Returns {Function}
*/
SQLConnector.prototype.getDefaultIdType = function(prop) {
return Number;
};
/**
* Get the default database schema name
* @returns {string} The default schema name, such as 'public' or 'dbo'
*/
SQLConnector.prototype.getDefaultSchemaName = function() {
return '';
};
/**
* Get the database schema name for the given model. The schema name can be
* customized at model settings or connector configuration level as `schema` or
* `schemaName`. For example,
*
* ```json
* "Customer": {
* "name": "Customer",
* "mysql": {
* "schema": "MYDB",
* "table": "CUSTOMER"
* }
* }
* ```
*
* @param {String} model The model name
* @returns {String} The database schema name
*/
SQLConnector.prototype.schema = function(model) {
// Check if there is a 'schema' property for connector
var dbMeta = this.getConnectorSpecificSettings(model);
var schemaName = (dbMeta && (dbMeta.schema || dbMeta.schemaName)) ||
(this.settings.schema || this.settings.schemaName) ||
this.getDefaultSchemaName();
return schemaName;
};
/**
* Get the table name for the given model. The table name can be customized
* at model settings as `table` or `tableName`. For example,
*
* ```json
* "Customer": {
* "name": "Customer",
* "mysql": {
* "table": "CUSTOMER"
* }
* }
* ```
*
* Returns the table name (String).
* @param {String} model The model name
*/
SQLConnector.prototype.table = function(model) {
var dbMeta = this.getConnectorSpecificSettings(model);
var tableName;
if (dbMeta) {
tableName = dbMeta.table || dbMeta.tableName;
if (tableName) {
// Explicit table name, return as-is
return tableName;
}
}
tableName = model;
if (typeof this.dbName === 'function') {
tableName = this.dbName(tableName);
}
return tableName;
};
/**
* Get the column name for the given model property. The column name can be
* customized at the model property definition level as `column` or
* `columnName`. For example,
*
* ```json
* "name": {
* "type": "string",
* "mysql": {
* "column": "NAME"
* }
* }
* ```
*
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The column name
*/
SQLConnector.prototype.column = function(model, property) {
var prop = this.getPropertyDefinition(model, property);
var columnName;
if (prop && prop[this.name]) {
columnName = prop[this.name].column || prop[this.name].columnName;
if (columnName) {
// Explicit column name, return as-is
return columnName;
}
}
columnName = property;
if (typeof this.dbName === 'function') {
columnName = this.dbName(columnName);
}
return columnName;
};
/**
* Get the column metadata for the given model property
* @param {String} model The model name
* @param {String} property The property name
* @returns {Object} The column metadata
*/
SQLConnector.prototype.columnMetadata = function(model, property) {
return this.getDataSource(model).columnMetadata(model, property);
};
/**
* Get the corresponding property name for the given column name
* @param {String} model The model name
* @param {String} column The column name
* @returns {String} The property name for a given column
*/
SQLConnector.prototype.propertyName = function(model, column) {
var props = this.getModelDefinition(model).properties;
for (var p in props) {
if (this.column(model, p) === column) {
return p;
}
}
return null;
};
/**
* Get the id column name
* @param {String} model The model name
* @returns {String} The id column name
*/
SQLConnector.prototype.idColumn = function(model) {
var name = this.getDataSource(model).idColumnName(model);
var dbName = this.dbName;
if (typeof dbName === 'function') {
name = dbName(name);
}
return name;
};
/**
* Get the escaped id column name
* @param {String} model The model name
* @returns {String} the escaped id column name
*/
SQLConnector.prototype.idColumnEscaped = function(model) {
return this.escapeName(this.idColumn(model));
};
/**
* Get the escaped table name
* @param {String} model The model name
* @returns {String} the escaped table name
*/
SQLConnector.prototype.tableEscaped = function(model) {
return this.escapeName(this.table(model));
};
/**
* Get the escaped column name for a given model property
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The escaped column name
*/
SQLConnector.prototype.columnEscaped = function(model, property) {
return this.escapeName(this.column(model, property));
};
/*!
* Check if id value is set
* @param idValue
* @param cb
* @param returningNull
* @returns {boolean}
*/
function isIdValuePresent(idValue, cb, returningNull) {
try {
assert(idValue !== null && idValue !== undefined, 'id value is required');
return true;
} catch (err) {
process.nextTick(function() {
if (cb) cb(returningNull ? null : err);
});
return false;
}
}
/**
* Convert the id value to the form required by database column
* @param {String} model The model name
* @param {*} idValue The id property value
* @returns {*} The escaped id column value
*/
SQLConnector.prototype.idColumnValue = function(model, idValue) {
var idProp = this.getDataSource(model).idProperty(model);
if (typeof this.toColumnValue === 'function') {
return this.toColumnValue(idProp, idValue);
} else {
return idValue;
}
};
/**
* Replace `?` with connector specific placeholders. For example,
*
* ```
* {sql: 'SELECT * FROM CUSTOMER WHERE NAME=?', params: ['John']}
* ==>
* {sql: 'SELECT * FROM CUSTOMER WHERE NAME=:1', params: ['John']}
* ```
* *LIMITATION*: We don't handle the ? inside escaped values, for example,
* `SELECT * FROM CUSTOMER WHERE NAME='J?hn'` will not be parameterized
* correctly.
*
* @param {ParameterizedSQL|Object} ps Parameterized SQL
* @returns {ParameterizedSQL} Parameterized SQL with the connector specific
* placeholders
*/
SQLConnector.prototype.parameterize = function(ps) {
ps = new ParameterizedSQL(ps);
// The value is parameterized, for example
// {sql: 'to_point(?,?)', values: [1, 2]}
var parts = ps.sql.split(PLACEHOLDER);
var clause = [];
for (var j = 0, m = parts.length; j < m; j++) {
// Replace ? with the keyed placeholder, such as :5
clause.push(parts[j]);
if (j !== parts.length - 1) {
clause.push(this.getPlaceholderForValue(j + 1));
}
}
ps.sql = clause.join('');
return ps;
};
/**
* Build the the `INSERT INTO` statement
* @param {String} model The model name
* @param {Object} fields Fields to be inserted
* @param {Object} options Options object
* @returns {ParameterizedSQL}
*/
SQLConnector.prototype.buildInsertInto = function(model, fields, options) {
var stmt = new ParameterizedSQL('INSERT INTO ' + this.tableEscaped(model));
var columnNames = fields.names.join(',');
if (columnNames) {
stmt.merge('(' + columnNames + ')', '');
}
return stmt;
};
/**
* Build the clause to return id values after insert
* @param {String} model The model name
* @param {Object} data The model data object
* @param {Object} options Options object
* @returns {string}
*/
SQLConnector.prototype.buildInsertReturning = function(model, data, options) {
return '';
};
/**
* Build the clause for default values if the fields is empty
* @param {String} model The model name
* @param {Object} data The model data object
* @param {Object} options Options object
* @returns {string} 'DEFAULT VALUES'
*/
SQLConnector.prototype.buildInsertDefaultValues = function(model, data, options) {
return 'VALUES()';
};
/**
* Build INSERT SQL statement
* @param {String} model The model name
* @param {Object} data The model data object
* @param {Object} options The options object
* @returns {string} The INSERT SQL statement
*/
SQLConnector.prototype.buildInsert = function(model, data, options) {
var fields = this.buildFields(model, data);
var insertStmt = this.buildInsertInto(model, fields, options);
var columnValues = fields.columnValues;
var fieldNames = fields.names;
if (fieldNames.length) {
var values = ParameterizedSQL.join(columnValues, ',');
values.sql = 'VALUES(' + values.sql + ')';
insertStmt.merge(values);
} else {
insertStmt.merge(this.buildInsertDefaultValues(model, data, options));
}
var returning = this.buildInsertReturning(model, data, options);
if (returning) {
insertStmt.merge(returning);
}
return this.parameterize(insertStmt);
};
/**
* Execute a SQL statement with given parameters.
*
* @param {String} sql The SQL statement
* @param {*[]} [params] An array of parameter values
* @param {Object} [options] Options object
* @param {Function} [callback] The callback function
*/
SQLConnector.prototype.execute = function(sql, params, options, callback) {
assert(typeof sql === 'string', 'sql must be a string');
if (typeof params === 'function' && options === undefined &&
callback === undefined) {
// execute(sql, callback)
options = {};
callback = params;
params = [];
} else if (typeof options === 'function' && callback === undefined) {
// execute(sql, params, callback)
callback = options;
options = {};
}
params = params || [];
options = options || {};
assert(Array.isArray(params), 'params must be an array');
assert(typeof options === 'object', 'options must be an object');
assert(typeof callback === 'function', 'callback must be a function');
var self = this;
if (!this.dataSource.connected) {
return this.dataSource.once('connected', function() {
self.execute(sql, params, options, callback);
});
}
var context = {
req: {
sql: sql,
params: params,
},
options: options,
};
this.notifyObserversAround('execute', context, function(context, done) {
self.executeSQL(context.req.sql, context.req.params, context.options,
function(err, info) {
if (err) {
debug('Error: %j %j %j', err, context.req.sql, context.req.params);
}
if (!err && info != null) {
context.res = info;
}
// Don't pass more than one args as it will confuse async.waterfall
done(err, info);
});
}, callback);
};
/**
* Create the data model in MySQL
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {Object} options Options object
* @param {Function} [callback] The callback function
*/
SQLConnector.prototype.create = function(model, data, options, callback) {
var self = this;
var stmt = this.buildInsert(model, data, options);
this.execute(stmt.sql, stmt.params, options, function(err, info) {
if (err) {
callback(err);
} else {
var insertedId = self.getInsertedId(model, info);
callback(err, insertedId);
}
});
};
/**
* Save the model instance into the database
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {Object} options Options object
* @param {Function} cb The callback function
*/
SQLConnector.prototype.save = function(model, data, options, cb) {
var idName = this.idName(model);
var idValue = data[idName];
if (!isIdValuePresent(idValue, cb)) {
return;
}
var where = {};
where[idName] = idValue;
var updateStmt = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model));
updateStmt.merge(this.buildFieldsForUpdate(model, data));
var whereStmt = this.buildWhere(model, where);
updateStmt.merge(whereStmt);
updateStmt = this.parameterize(updateStmt);
this.execute(updateStmt.sql, updateStmt.params, options,
function(err, result) {
if (cb) cb(err, result);
});
};
/**
* Check if a model instance exists for the given id value
* @param {String} model The model name
* @param {*} id The id value
* @param {Object} options Options object
* @param {Function} cb The callback function
*/
SQLConnector.prototype.exists = function(model, id, options, cb) {
if (!isIdValuePresent(id, cb, true)) {
return;
}
var idName = this.idName(model);
var where = {};
where[idName] = id;
var selectStmt = new ParameterizedSQL(
'SELECT 1 FROM ' + this.tableEscaped(model) +
' WHERE ' + this.idColumnEscaped(model)
);
selectStmt.merge(this.buildWhere(model, where));
selectStmt = this.applyPagination(model, selectStmt, {
limit: 1,
offset: 0,
order: [idName],
});
selectStmt = this.parameterize(selectStmt);
this.execute(selectStmt.sql, selectStmt.params, options, function(err, data) {
if (!cb) return;
if (err) {
cb(err);
} else {
cb(null, data.length >= 1);
}
});
};
/**
* ATM, this method is not used by loopback-datasource-juggler dao, which
* maps `destroy` to `destroyAll` with a `where` filter that includes the `id`
* instead.
*
* Delete a model instance by id value
* @param {String} model The model name
* @param {*} id The id value
* @param {Object} options Options object
* @param {Function} cb The callback function
* @private
*/
SQLConnector.prototype.destroy = function(model, id, options, cb) {
if (!isIdValuePresent(id, cb, true)) {
return;
}
var idName = this.idName(model);
var where = {};
where[idName] = id;
this.destroyAll(model, where, options, cb);
};
// Alias to `destroy`. Juggler checks `destroy` only.
Connector.defineAliases(SQLConnector.prototype, 'destroy',
['delete', 'deleteById', 'destroyById']);
/**
* Build the `DELETE FROM` SQL statement
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} options Options object
* @returns {ParameterizedSQL} The SQL DELETE FROM statement
*/
SQLConnector.prototype.buildDelete = function(model, where, options) {
var deleteStmt = new ParameterizedSQL('DELETE FROM ' +
this.tableEscaped(model));
deleteStmt.merge(this.buildWhere(model, where));
return this.parameterize(deleteStmt);
};
/**
* Delete all matching model instances
*
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} options The options object
* @param {Function} cb The callback function
*/
SQLConnector.prototype.destroyAll = function(model, where, options, cb) {
var stmt = this.buildDelete(model, where, options);
this._executeAlteringQuery(model, stmt.sql, stmt.params, options, cb || NOOP);
};
// Alias to `destroyAll`. Juggler checks `destroyAll` only.
Connector.defineAliases(SQLConnector.prototype, 'destroyAll', ['deleteAll']);
/**
* ATM, this method is not used by loopback-datasource-juggler dao, which
* maps `updateAttributes` to `update` with a `where` filter that includes the
* `id` instead.
*
* Update attributes for a given model instance
* @param {String} model The model name
* @param {*} id The id value
* @param {Object} data The model data instance containing all properties to
* be updated
* @param {Object} options Options object
* @param {Function} cb The callback function
* @private
*/
SQLConnector.prototype.updateAttributes = function(model, id, data, options, cb) {
if (!isIdValuePresent(id, cb)) return;
var where = this._buildWhereObjById(model, id, data);
this.updateAll(model, where, data, options, cb);
};
/**
* Replace attributes for a given model instance
* @param {String} model The model name
* @param {*} id The id value
* @param {Object} data The model data instance containing all properties to
* be replaced
* @param {Object} options Options object
* @param {Function} cb The callback function
* @private
*/
SQLConnector.prototype.replaceById = function(model, id, data, options, cb) {
if (!isIdValuePresent(id, cb)) return;
var where = this._buildWhereObjById(model, id, data);
this._replace(model, where, data, options, cb);
};
/*
* @param model The model name.
* @param id The instance ID.
* @param {Object} data The data Object.
* @returns {Object} where The where object for a spcific instance.
* @private
*/
SQLConnector.prototype._buildWhereObjById = function(model, id, data) {
var idName = this.idName(model);
delete data[idName];
var where = {};
where[idName] = id;
return where;
};
/**
* Build the UPDATE statement
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} data The data to be changed
* @param {Object} options The options object
* @param {Function} cb The callback function
* @returns {ParameterizedSQL} The UPDATE SQL statement
*/
SQLConnector.prototype.buildUpdate = function(model, where, data, options) {
var fields = this.buildFieldsForUpdate(model, data);
return this._constructUpdateQuery(model, where, fields);
};
/**
* Build the UPDATE statement for replacing
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} data The data to be changed
* @param {Object} options The options object
* @param {Function} cb The callback function
* @returns {ParameterizedSQL} The UPDATE SQL statement for replacing fields
*/
SQLConnector.prototype.buildReplace = function(model, where, data, options) {
var fields = this.buildFieldsForReplace(model, data);
return this._constructUpdateQuery(model, where, fields);
};
/*
* @param model The model name.
* @param {} where The where object.
* @param {Object} field The parameterizedSQL fileds.
* @returns {Object} update query Constructed update query.
* @private
*/
SQLConnector.prototype._constructUpdateQuery = function(model, where, fields) {
var updateClause = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model));
var whereClause = this.buildWhere(model, where);
updateClause.merge([fields, whereClause]);
return this.parameterize(updateClause);
};
/**
* Update all instances that match the where clause with the given data
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} data The property/value object representing changes
* to be made
* @param {Object} options The options object
* @param {Function} cb The callback function
*/
SQLConnector.prototype.update = function(model, where, data, options, cb) {
var stmt = this.buildUpdate(model, where, data, options);
this._executeAlteringQuery(model, stmt.sql, stmt.params, options, cb || NOOP);
};
/**
* Replace all instances that match the where clause with the given data
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} data The property/value object representing changes
* to be made
* @param {Object} options The options object
* @param {Function} cb The callback function
*/
SQLConnector.prototype._replace = function(model, where, data, options, cb) {
var self = this;
var stmt = this.buildReplace(model, where, data, options);
this.execute(stmt.sql, stmt.params, options, function(err, info) {
if (err) return cb(err);
var affectedRows = self.getCountForAffectedRows(model, info);
var rowCount = typeof (affectedRows) === 'number' ?
affectedRows : info.affectedRows;
if (rowCount === 0) {
return cb(errorIdNotFoundForReplace(where.id));
} else {
return cb(null, info);
}
});
};
function errorIdNotFoundForReplace(idValue) {
var msg = g.f('Could not replace. Object with id %s does not exist!', idValue);
var error = new Error(msg);
error.statusCode = error.status = 404;
return error;
}
SQLConnector.prototype._executeAlteringQuery = function(model, sql, params, options, cb) {
var self = this;
this.execute(sql, params, options, function(err, info) {
var affectedRows = self.getCountForAffectedRows(model, info);
cb(err, {count: affectedRows});
});
};
// Alias to `update` and `replace`. Juggler checks `update` and `replace` only.
Connector.defineAliases(SQLConnector.prototype, 'update', ['updateAll']);
Connector.defineAliases(SQLConnector.prototype, 'replace', ['replaceAll']);
/**
* Build the SQL WHERE clause for the where object
* @param {string} model Model name
* @param {object} where An object for the where conditions
* @returns {ParameterizedSQL} The SQL WHERE clause
*/
SQLConnector.prototype.buildWhere = function(model, where) {
var whereClause = this._buildWhere(model, where);
if (whereClause.sql) {
whereClause.sql = 'WHERE ' + whereClause.sql;
}
return whereClause;
};
/**
* Build SQL expression
* @param {String} columnName Escaped column name
* @param {String} operator SQL operator
* @param {*} columnValue Column value
* @param {*} propertyValue Property value
* @returns {ParameterizedSQL} The SQL expression
*/
SQLConnector.prototype.buildExpression =
function(columnName, operator, columnValue, propertyValue) {
function buildClause(columnValue, separator, grouping) {
var values = [];
for (var i = 0, n = columnValue.length; i < n; i++) {
if (columnValue[i] instanceof ParameterizedSQL) {
values.push(columnValue[i]);
} else {
values.push(new ParameterizedSQL(PLACEHOLDER, [columnValue[i]]));
}
}
separator = separator || ',';
var clause = ParameterizedSQL.join(values, separator);
if (grouping) {
clause.sql = '(' + clause.sql + ')';
}
return clause;
}
var sqlExp = columnName;
var clause;
if (columnValue instanceof ParameterizedSQL) {
clause = columnValue;
} else {
clause = new ParameterizedSQL(PLACEHOLDER, [columnValue]);
}
switch (operator) {
case 'gt':
sqlExp += '>';
break;
case 'gte':
sqlExp += '>=';
break;
case 'lt':
sqlExp += '<';
break;
case 'lte':
sqlExp += '<=';
break;
case 'between':
sqlExp += ' BETWEEN ';
clause = buildClause(columnValue, ' AND ', false);
break;
case 'inq':
sqlExp += ' IN ';
clause = buildClause(columnValue, ',', true);
break;
case 'nin':
sqlExp += ' NOT IN ';
clause = buildClause(columnValue, ',', true);
break;
case 'neq':
if (columnValue == null) {
return new ParameterizedSQL(sqlExp + ' IS NOT NULL');
}
sqlExp += '!=';
break;
case 'like':
sqlExp += ' LIKE ';
break;
case 'nlike':
sqlExp += ' NOT LIKE ';
break;
// this case not needed since each database has its own regex syntax, but
// we leave the MySQL syntax here as a placeholder
case 'regexp':
sqlExp += ' REGEXP ';
break;
}
var stmt = ParameterizedSQL.join([sqlExp, clause], '');
return stmt;
};
/*!
* @param model
* @param where
* @returns {ParameterizedSQL}
* @private
*/
SQLConnector.prototype._buildWhere = function(model, where) {
if (!where) {
return new ParameterizedSQL('');
}
if (typeof where !== 'object' || Array.isArray(where)) {
debug('Invalid value for where: %j', where);
return new ParameterizedSQL('');
}
var self = this;
var props = self.getModelDefinition(model).properties;
var whereStmts = [];
for (var key in where) {
var stmt = new ParameterizedSQL('', []);
// Handle and/or operators
if (key === 'and' || key === 'or') {
var branches = [];
var branchParams = [];
var clauses = where[key];
if (Array.isArray(clauses)) {
for (var i = 0, n = clauses.length; i < n; i++) {
var stmtForClause = self._buildWhere(model, clauses[i]);
if (stmtForClause.sql) {
stmtForClause.sql = '(' + stmtForClause.sql + ')';
branchParams = branchParams.concat(stmtForClause.params);
branches.push(stmtForClause.sql);
}
}
stmt.merge({
sql: branches.join(' ' + key.toUpperCase() + ' '),
params: branchParams,
});
whereStmts.push(stmt);
continue;
}
// The value is not an array, fall back to regular fields
}
var p = props[key];
if (p == null) {
// Unknown property, ignore it
debug('Unknown property %s is skipped for model %s', key, model);
continue;
}
/* eslint-disable one-var */
var columnName = self.columnEscaped(model, key);
var expression = where[key];
var columnValue;
var sqlExp;
/* eslint-enable one-var */
if (expression === null || expression === undefined) {
stmt.merge(columnName + ' IS NULL');
} else if (expression && expression.constructor === Object) {
var operator = Object.keys(expression)[0];
// Get the expression without the operator
expression = expression[operator];
if (operator === 'inq' || operator === 'nin' || operator === 'between') {
columnValue = [];
if (Array.isArray(expression)) {
// Column value is a list
for (var j = 0, m = expression.length; j < m; j++) {
columnValue.push(this.toColumnValue(p, expression[j]));
}
} else {
columnValue.push(this.toColumnValue(p, expression));
}
if (operator === 'between') {
// BETWEEN v1 AND v2
var v1 = columnValue[0] === undefined ? null : columnValue[0];
var v2 = columnValue[1] === undefined ? null : columnValue[1];
columnValue = [v1, v2];
} else {
// IN (v1,v2,v3) or NOT IN (v1,v2,v3)
if (columnValue.length === 0) {
if (operator === 'inq') {
columnValue = [null];
} else {
// nin () is true
continue;
}
}
}
} else if (operator === 'regexp' && expression instanceof RegExp) {
// do not coerce RegExp based on property definitions
columnValue = expression;
} else {
columnValue = this.toColumnValue(p, expression);
}
sqlExp = self.buildExpression(
columnName, operator, columnValue, p);
stmt.merge(sqlExp);
} else {
// The expression is the field value, not a condition
columnValue = self.toColumnValue(p, expression);
if (columnValue === null) {
stmt.merge(columnName + ' IS NULL');
} else {
if (columnValue instanceof ParameterizedSQL) {
stmt.merge(columnName + '=').merge(columnValue);
} else {
stmt.merge({
sql: columnName + '=?',
params: [columnValue],
});
}
}
}
whereStmts.push(stmt);
}
var params = [];
var sqls = [];
for (var k = 0, s = whereStmts.length; k < s; k++) {
sqls.push(whereStmts[k].sql);
params = params.concat(whereStmts[k].params);
}
var whereStmt = new ParameterizedSQL({
sql: sqls.join(' AND '),
params: params,
});
return whereStmt;
};
/**
* Build the ORDER BY clause
* @param {string} model Model name
* @param {string[]} order An array of sorting criteria
* @returns {string} The ORDER BY clause
*/
SQLConnector.prototype.buildOrderBy = function(model, order) {
if (!order) {
return '';
}
var self = this;
if (typeof order === 'string') {
order = [order];
}
var clauses = [];
for (var i = 0, n = order.length; i < n; i++) {
var t = order[i].split(/[\s,]+/);
if (t.length === 1) {
clauses.push(self.columnEscaped(model, order[i]));
} else {
clauses.push(self.columnEscaped(model, t[0]) + ' ' + t[1]);
}
}
return 'ORDER BY ' + clauses.join(',');
};
/**
* Build an array of fields for the database operation
* @param {String} model Model name
* @param {Object} data Model data object
* @param {Boolean} excludeIds Exclude id properties or not, default to false
* @returns {{names: Array, values: Array, properties: Array}}
*/
SQLConnector.prototype.buildFields = function(model, data, excludeIds) {
var keys = Object.keys(data);
return this._buildFieldsForKeys(model, data, keys, excludeIds);
};
/**
* Build an array of fields for the replace database operation
* @param {String} model Model name
* @param {Object} data Model data object
* @param {Boolean} excludeIds Exclude id properties or not, default to false
* @returns {{names: Array, values: Array, properties: Array}}
*/
SQLConnector.prototype.buildReplaceFields = function(model, data, excludeIds) {
var props = this.getModelDefinition(model).properties;
var keys = Object.keys(props);
return this._buildFieldsForKeys(model, data, keys, excludeIds);
};
/*
* @param {String} model The model name.
* @returns {Object} data The model data object.
* @returns {Array} keys The key fields for which need to be built.
* @param {Boolean} excludeIds Exclude id properties or not, default to false
* @private
*/
SQLConnector.prototype._buildFieldsForKeys = function(model, data, keys, excludeIds) {
var props = this.getModelDefinition(model).properties;
var fields = {
names: [], // field names
columnValues: [], // an array of ParameterizedSQL
properties: [], // model properties
};
for (var i = 0, n = keys.length; i < n; i++) {
var key = keys[i];
var p = props[key];
if (p == null) {
// Unknown property, ignore it
debug('Unknown property %s is skipped for model %s', key, model);
continue;
}
if (excludeIds && p.id) {
continue;
}
var k = this.columnEscaped(model, key);
var v = this.toColumnValue(p, data[key]);
if (v !== undefined) {
fields.names.push(k);
if (v instanceof ParameterizedSQL) {
fields.columnValues.push(v);
} else {
fields.columnValues.push(new ParameterizedSQL(PLACEHOLDER, [v]));
}
fields.properties.push(p);
}
}
return fields;
};
/**
* Build the SET clause for database update.
* @param {String} model Model name.
* @param {Object} data The model data object.
* @param {Boolean} excludeIds Exclude id properties or not, default to true.
* @returns {string} The list of fields for update query.
*/
SQLConnector.prototype.buildFieldsForUpdate = function(model, data, excludeIds) {
if (excludeIds === undefined) {
excludeIds = true;
}
var fields = this.buildFields(model, data, excludeIds);
return this._constructUpdateParameterizedSQL(fields);
};
/**
* Build the SET clause for database replace through update query.
* @param {String} model Model name.
* @param {Object} data The model data object.
* @param {Boolean} excludeIds Exclude id properties or not, default to true.
* @returns {string} The list of fields for update query.
*/
SQLConnector.prototype.buildFieldsForReplace = function(model, data, excludeIds) {
if (excludeIds === undefined) {
excludeIds = true;
}
var fields = this.buildReplaceFields(model, data, excludeIds);
return this._constructUpdateParameterizedSQL(fields);
};
/*
* @param {Object} field The fileds.
* @returns {Object} parameterizedSQL.
* @private
*/
SQLConnector.prototype._constructUpdateParameterizedSQL = function(fields) {
var columns = new ParameterizedSQL('');
for (var i = 0, n = fields.names.length; i < n; i++) {
var clause = ParameterizedSQL.append(fields.names[i],
fields.columnValues[i], '=');
columns.merge(clause, ',');
}
columns.sql = 'SET ' + columns.sql;
return columns;
};
/**
* Build a list of escaped column names for the given model and fields filter
* @param {string} model Model name
* @param {object} filter The filter object
* @returns {string} Comma separated string of escaped column names
*/
SQLConnector.prototype.buildColumnNames = function(model, filter) {
var fieldsFilter = filter && filter.fields;
var cols = this.getModelDefinition(model).properties;
if (!cols) {
return '*';
}
var self = this;
var keys = Object.keys(cols);
if (Array.isArray(fieldsFilter) && fieldsFilter.length > 0) {
// Not empty array, including all the fields that are valid properties
keys = fieldsFilter.filter(function(f) {
return cols[f];
});
} else if ('object' === typeof fieldsFilter &&
Object.keys(fieldsFilter).length > 0) {
// { field1: boolean, field2: boolean ... }
var included = [];
var excluded = [];
keys.forEach(function(k) {
if (fieldsFilter[k]) {
included.push(k);
} else if ((k in fieldsFilter) && !fieldsFilter[k]) {
excluded.push(k);
}
});
if (included.length > 0) {
keys = included;
} else if (excluded.length > 0) {
excluded.forEach(function(e) {
var index = keys.indexOf(e);
keys.splice(index, 1);
});
}
}
var names = keys.map(function(c) {
return self.columnEscaped(model, c);
});
return names.join(',');
};
/**
* Build a SQL SELECT statement
* @param {String} model Model name
* @param {Object} filter Filter object
* @param {Object} options Options object
* @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]}
*/
SQLConnector.prototype.buildSelect = function(model, filter, options) {
if (!filter.order) {
var idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
}
var selectStmt = new ParameterizedSQL('SELECT ' +
this.buildColumnNames(model, filter) +
' FROM ' + this.tableEscaped(model)
);
if (filter) {
if (filter.where) {
var whereStmt = this.buildWhere(model, filter.where);
selectStmt.merge(whereStmt);
}
if (filter.order) {
selectStmt.merge(this.buildOrderBy(model, filter.order));
}
if (filter.limit || filter.skip || filter.offset) {
selectStmt = this.applyPagination(
model, selectStmt, filter);
}
}
return this.parameterize(selectStmt);
};
/**
* Transform the row data into a model data object
* @param {string} model Model name
* @param {object} rowData An object representing the row data from DB
* @returns {object} Model data object
*/
SQLConnector.prototype.fromRow = SQLConnector.prototype.fromDatabase =
function(model, rowData) {
if (rowData == null) {
return rowData;
}
var props = this.getModelDefinition(model).properties;
var data = {};
for (var p in props) {
var columnName = this.column(model, p);
// Load properties from the row
var columnValue = this.fromColumnValue(props[p], rowData[columnName]);
if (columnValue !== undefined) {
data[p] = columnValue;
}
}
return data;
};
/**
* Find matching model instances by the filter
*
* Please also note the name `all` is confusing. `Model.find` is to find all
* matching instances while `Model.findById` is to find an instance by id. On
* the other hand, `Connector.prototype.all` implements `Model.find` while
* `Connector.prototype.find` implements `Model.findById` due to the `bad`
* naming convention we inherited from juggling-db.
*
* @param {String} model The model name
* @param {Object} filter The filter
* @param {Function} [cb] The cb function
*/
SQLConnector.prototype.all = function find(model, filter, options, cb) {
var self = this;
// Order by id if no order is specified
filter = filter || {};
var stmt = this.buildSelect(model, filter, options);
this.execute(stmt.sql, stmt.params, options, function(err, data) {
if (err) {
return cb(err, []);
}
var objs = data.map(function(obj) {
return self.fromRow(model, obj);
});
if (filter && filter.include) {
self.getModelDefinition(model).model.include(
objs, filter.include, options, cb);
} else {
cb(null, objs);
}
});
};
// Alias to `all`. Juggler checks `all` only.
Connector.defineAliases(SQLConnector.prototype, 'all', ['findAll']);
/**
* ATM, this method is not used by loopback-datasource-juggler dao, which
* maps `findById` to `find` with a `where` filter that includes the `id`
* instead.
*
* Please also note the name `find` is confusing. `Model.find` is to find all
* matching instances while `Model.findById` is to find an instance by id. On
* the other hand, `Connector.prototype.find` is for `findById` and
* `Connector.prototype.all` is for `find` due the `bad` convention used by
* juggling-db.
*
* Find by id
* @param {String} model The Model name
* @param {*} id The id value
* @param {Object} options The options object
* @param {Function} cb The callback function
* @private
*/
SQLConnector.prototype.find = function(model, id, options, cb) {
if (id == null) {
process.nextTick(function() {
var err = new Error(g.f('id value is required'));
if (cb) {
cb(err);
}
});
return;
}
var where = {};
var idName = this.idName(model);
where[idName] = id;
var filter = {limit: 1, offset: 0, order: idName, where: where};
return this.all(model, filter, options, function(err, results) {
cb(err, (results && results[0]) || null);
});
};
// Alias to `find`. Juggler checks `findById` only.
Connector.defineAliases(SQLConnector.prototype, 'find', ['findById']);
/**
* Count all model instances by the where filter
*
* @param {String} model The model name
* @param {Object} where The where object
* @param {Object} options The options object
* @param {Function} cb The callback function
*/
SQLConnector.prototype.count = function(model, where, options, cb) {
if (typeof where === 'function') {
// Backward compatibility for 1.x style signature:
// count(model, cb, where)
var tmp = options;
cb = where;
where = tmp;
}
var stmt = new ParameterizedSQL('SELECT count(*) as "cnt" FROM ' +
this.tableEscaped(model));
stmt = stmt.merge(this.buildWhere(model, where));
stmt = this.parameterize(stmt);
this.execute(stmt.sql, stmt.params,
function(err, res) {
if (err) {
return cb(err);
}
var c = (res && res[0] && res[0].cnt) || 0;
// Some drivers return count as a string to contain bigint
// See https://github.com/brianc/node-postgres/pull/427
cb(err, Number(c));
});
};
/**
* Drop the table for the given model from the database
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.dropTable = function(model, cb) {
this.execute('DROP TABLE IF EXISTS ' + this.tableEscaped(model), cb);
};
/**
* Create the table for the given model
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.createTable = function(model, cb) {
var sql = 'CREATE TABLE ' + this.tableEscaped(model) +
' (\n ' + this.buildColumnDefinitions(model) + '\n)';
this.execute(sql, cb);
};
/**
* Recreate the tables for the given models
* @param {[String]|String} [models] A model name or an array of model names,
* if not present, apply to all models defined in the connector
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.automigrate = function(models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(self._models);
if (models.length === 0) {
return process.nextTick(cb);
}
var invalidModels = models.filter(function(m) {
return !(m in self._models);
});
if (invalidModels.length) {
return process.nextTick(function() {
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
}
async.each(models, function(model, done) {
self.dropTable(model, function(err) {
if (err) {
// TODO(bajtos) should we abort here and call cb(err)?
// The original code in juggler ignored the error completely
console.error(err);
}
self.createTable(model, function(err, result) {
if (err) {
console.error(err);
}
done(err, result);
});
});
}, cb);
};
/**
* Serialize an object into JSON string or other primitive types so that it
* can be saved into a RDB column
* @param {Object} obj The object value
* @returns {*}
*/
SQLConnector.prototype.serializeObject = function(obj) {
var val;
if (obj && typeof obj.toJSON === 'function') {
obj = obj.toJSON();
}
if (typeof obj !== 'string') {
val = JSON.stringify(obj);
} else {
val = obj;
}
return val;
};
/*!
* @param obj
*/
SQLConnector.prototype.escapeObject = function(obj) {
var val = this.serializeObject(obj);
return this.escapeValue(val);
};
/**
* The following _abstract_ methods have to be implemented by connectors that
* extend from SQLConnector to reuse the base implementations of CRUD methods
* from SQLConnector
*/
/**
* Converts a model property value into the form required by the
* database column. The result should be one of following forms:
*
* - {sql: "point(?,?)", params:[10,20]}
* - {sql: "'John'", params: []}
* - "John"
*
* @param {Object} propertyDef Model property definition
* @param {*} value Model property value
* @returns {ParameterizedSQL|*} Database column value.
*
*/
SQLConnector.prototype.toColumnValue = function(propertyDef, value) {
throw new Error(g.f('{{toColumnValue()}} must be implemented by the connector'));
};
/**
* Convert the data from database column to model property
* @param {object} propertyDef Model property definition
* @param {*) value Column value
* @returns {*} Model property value
*/
SQLConnector.prototype.fromColumnValue = function(propertyDef, value) {
throw new Error(g.f('{{fromColumnValue()}} must be implemented by the connector'));
};
/**
* Escape the name for the underlying database
* @param {String} name The name
* @returns {String} An escaped name for SQL
*/
SQLConnector.prototype.escapeName = function(name) {
throw new Error(g.f('{{escapeName()}} must be implemented by the connector'));
};
/**
* Escape the name for the underlying database
* @param {String} value The value to be escaped
* @returns {*} An escaped value for SQL
*/
SQLConnector.prototype.escapeValue = function(value) {
throw new Error(g.f('{{escapeValue()}} must be implemented by the connector'));
};
/**
* Get the place holder in SQL for identifiers, such as ??
* @param {String} key Optional key, such as 1 or id
* @returns {String} The place holder
*/
SQLConnector.prototype.getPlaceholderForIdentifier = function(key) {
throw new Error(g.f('{{getPlaceholderForIdentifier()}} must be implemented by ' +
'the connector'));
};
/**
* Get the place holder in SQL for values, such as :1 or ?
* @param {String} key Optional key, such as 1 or id
* @returns {String} The place holder
*/
SQLConnector.prototype.getPlaceholderForValue = function(key) {
throw new Error(g.f('{{getPlaceholderForValue()}} must be implemented by ' +
'the connector'));
};
/**
* Build a new SQL statement with pagination support by wrapping the given sql
* @param {String} model The model name
* @param {ParameterizedSQL} stmt The sql statement
* @param {Object} filter The filter object from the query
*/
SQLConnector.prototype.applyPagination = function(model, stmt, filter) {
throw new Error(g.f('{{applyPagination()}} must be implemented by the connector'));
};
/**
* Parse the result for SQL UPDATE/DELETE/INSERT for the number of rows
* affected
* @param {String} model Model name
* @param {Object} info Status object
* @returns {Number} Number of rows affected
*/
SQLConnector.prototype.getCountForAffectedRows = function(model, info) {
throw new Error(g.f('{{getCountForAffectedRows()}} must be implemented by ' +
'the connector'));
};
/**
* Parse the result for SQL INSERT for newly inserted id
* @param {String} model Model name
* @param {Object} info The status object from driver
* @returns {*} The inserted id value
*/
SQLConnector.prototype.getInsertedId = function(model, info) {
throw new Error(g.f('{{getInsertedId()}} must be implemented by the connector'));
};
/**
* Execute a SQL statement with given parameters
* @param {String} sql The SQL statement
* @param {*[]} [params] An array of parameter values
* @param {Object} [options] Options object
* @param {Function} [callback] The callback function
*/
SQLConnector.prototype.executeSQL = function(sql, params, options, callback) {
throw new Error(g.f('{{executeSQL()}} must be implemented by the connector'));
};
// Refactored Discovery methods
/**
* Build sql for listing schemas
* @param {Object} options Options for discoverDatabaseSchemas
*/
SQLConnector.prototype.buildQuerySchemas = function(options) {
var sql = 'SELECT catalog_name as "catalog",' +
' schema_name as "schema"' +
' FROM information_schema.schemata';
return this.paginateSQL(sql, 'schema_name', options);
};
/**
* Paginate the results returned from database
* @param {String} sql The sql to execute
* @param {Object} orderBy The property name by which results are ordered
* @param {Object} options Options for discoverDatabaseSchemas
*/
SQLConnector.prototype.paginateSQL = function(sql, orderBy, options) {
throw new Error(g.f('{{paginateSQL}} must be implemented by the connector'));
};
/**
* Discover database schemas
*
// * @param {Object} options Options for discovery
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.discoverDatabaseSchemas = function(options, cb) {
if (!cb && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
var self = this;
this.execute(self.buildQuerySchemas(options), cb);
};
/*!
* Build sql for listing tables
* @param options {all: for all owners, owner: for a given owner}
* @returns {string} The sql statement
*/
// Due to the different implementation structure of information_schema across
// connectors, each connector will have to generate its own query
SQLConnector.prototype.buildQueryTables = function(options) {
throw new Error(g.f('{{buildQueryTables}} must be implemented by the connector'));
};
/*!
* Build sql for listing views
* @param options {all: for all owners, owner: for a given owner}
* @returns {string} The sql statement
*/
// Due to the different implementation structure of information_schema across
// connectors, each connector will have to generate its own query
SQLConnector.prototype.buildQueryViews = function(options) {
throw new Error(g.f('{{buildQueryViews}} must be implemented by the connector'));
};
/**
* Discover model definitions
*
* @param {Object} options Options for discovery
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.discoverModelDefinitions = function(options, cb) {
if (!cb && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
var self = this;
var calls = [function(callback) {
self.execute(self.buildQueryTables(options), callback);
}];
if (options.views) {
calls.push(function(callback) {
self.execute(self.buildQueryViews(options), callback);
});
}
async.parallel(calls, function(err, data) {
if (err) {
cb(err, data);
} else {
var merged = [];
merged = merged.concat(data.shift());
if (data.length) {
merged = merged.concat(data.shift());
}
cb(err, merged);
}
});
};
/**
* Build sql for listing columns
* @param {String} schema The schema name
* @param {String} table The table name
*/
// Due to the different implementation structure of information_schema across
// connectors, each connector will have to generate its own query
SQLConnector.prototype.buildQueryColumns = function(schema, table) {
throw new Error(g.f('{{buildQueryColumns}} must be implemented by the connector'));
};
/**
* Map the property type from database to loopback
* @param {Object} columnDefinition The columnDefinition of the table/schema
* @param {Object} options The options for the connector
*/
SQLConnector.prototype.buildPropertyType = function(columnDefinition, options) {
throw new Error(g.f('{{buildPropertyType}} must be implemented by the connector'));
};
/*!
* Normalize the arguments
* @param table string, required
* @param options object, optional
* @param cb function, optional
*/
SQLConnector.prototype.getArgs = function(table, options, cb) {
throw new Error(g.f('{{getArgs}} must be implemented by the connector'));
};
/**
* Discover model properties from a table
* @param {String} table The table name
* @param {Object} options The options for discovery
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.discoverModelProperties = function(table, options, cb) {
var self = this;
var args = self.getArgs(table, options, cb);
var schema = args.schema;
table = args.table;
options = args.options;
if (!schema) {
schema = self.getDefaultSchema();
}
self.setDefaultOptions(options);
cb = args.cb;
var sql = self.buildQueryColumns(schema, table);
var callback = function(err, results) {
if (err) {
cb(err, results);
} else {
results.map(function(r) {
r.type = self.buildPropertyType(r, options);
self.setNullableProperty(r);
});
cb(err, results);
}
};
this.execute(sql, callback);
};
/*!
* Build the sql statement for querying primary keys of a given table
* @param schema
* @param table
* @returns {string}
*/
// http://docs.oracle.com/javase/6/docs/api/java/sql/DatabaseMetaData.html
// #getPrimaryKeys(java.lang.String, java.lang.String, java.lang.String)
// Due to the different implementation structure of information_schema across
// connectors, each connector will have to generate its own query
SQLConnector.prototype.buildQueryPrimaryKeys = function(schema, table) {
throw new Error(g.f('{{buildQueryPrimaryKeys}} must be implemented by the connector'));
};
/**
* Discover primary keys for a given table
* @param {String} table The table name
* @param {Object} options The options for discovery
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.discoverPrimaryKeys = function(table, options, cb) {
var self = this;
var args = self.getArgs(table, options, cb);
var schema = args.schema;
if (typeof(self.getDefaultSchema) === 'function' && !schema) {
schema = self.getDefaultSchema();
}
table = args.table;
options = args.options;
cb = args.cb;
var sql = self.buildQueryPrimaryKeys(schema, table);
this.execute(sql, cb);
};
/*!
* Build the sql statement for querying foreign keys of a given table
* @param schema
* @param table
* @returns {string}
*/
// Due to the different implementation structure of information_schema across
// connectors, each connector will have to generate its own query
SQLConnector.prototype.buildQueryForeignKeys = function(schema, table) {
throw new Error(g.f('{{buildQueryForeignKeys}} must be implemented by the connector'));
};
/**
* Discover foreign keys for a given table
* @param {String} table The table name
* @param {Object} options The options for discovery
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.discoverForeignKeys = function(table, options, cb) {
var self = this;
var args = self.getArgs(table, options, cb);
var schema = args.schema;
if (typeof(self.getDefaultSchema) === 'function' && !schema) {
schema = self.getDefaultSchema();
}
table = args.table;
options = args.options;
cb = args.cb;
var sql = self.buildQueryForeignKeys(schema, table);
this.execute(sql, cb);
};
/*!
* Retrieves a description of the foreign key columns that reference the
* given table's primary key columns (the foreign keys exported by a table).
* They are ordered by fkTableOwner, fkTableName, and keySeq.
* @param schema
* @param table
* @returns {string}
*/
// Due to the different implementation structure of information_schema across
// connectors, each connector will have to generate its own query
SQLConnector.prototype.buildQueryExportedForeignKeys = function(schema, table) {
throw new Error(g.f('{{buildQueryExportedForeignKeys}} must be implemented by' +
'the connector'));
};
/**
* Discover foreign keys that reference to the primary key of this table
* @param {String} table The table name
* @param {Object} options The options for discovery
* @param {Function} [cb] The callback function
*/
SQLConnector.prototype.discoverExportedForeignKeys = function(table, options, cb) {
var self = this;
var args = self.getArgs(table, options, cb);
var schema = args.schema;
if (typeof(self.getDefaultSchema) === 'function' && !schema) {
schema = self.getDefaultSchema();
}
table = args.table;
options = args.options;
cb = args.cb;
var sql = self.buildQueryExportedForeignKeys(schema, table);
this.execute(sql, cb);
};
/**
* Discover default schema of a database
* @param {Object} options The options for discovery
*/
SQLConnector.prototype.getDefaultSchema = function(options) {
throw new Error(g.f('{{getDefaultSchema}} must be implemented by' +
'the connector'));
};
/**
* Set default options for the connector
* @param {Object} options The options for discovery
*/
SQLConnector.prototype.setDefaultOptions = function(options) {
throw new Error(g.f('{{setDefaultOptions}} must be implemented by' +
'the connector'));
};
/**
* Set the nullable value for the property
* @param {Object} property The property to set nullable
*/
SQLConnector.prototype.setNullableProperty = function(property) {
throw new Error(g.f('{{setNullableProperty}} must be implemented by' +
'the connector'));
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('loopback:connector:transaction');
module.exports = Transaction;
/**
* Create a new Transaction object
* @param {Connector} connector The connector instance
* @param {*} connection A connection to the DB
* @constructor
*/
function Transaction(connector, connection) {
this.connector = connector;
this.connection = connection;
EventEmitter.call(this);
}
util.inherits(Transaction, EventEmitter);
// Isolation levels
Transaction.SERIALIZABLE = 'SERIALIZABLE';
Transaction.REPEATABLE_READ = 'REPEATABLE READ';
Transaction.READ_COMMITTED = 'READ COMMITTED';
Transaction.READ_UNCOMMITTED = 'READ UNCOMMITTED';
Transaction.hookTypes = {
BEFORE_COMMIT: 'before commit',
AFTER_COMMIT: 'after commit',
BEFORE_ROLLBACK: 'before rollback',
AFTER_ROLLBACK: 'after rollback',
TIMEOUT: 'timeout',
};
/**
* Commit a transaction and release it back to the pool
* @param cb
* @returns {*}
*/
Transaction.prototype.commit = function(cb) {
return this.connector.commit(this.connection, cb);
};
/**
* Rollback a transaction and release it back to the pool
* @param cb
* @returns {*|boolean}
*/
Transaction.prototype.rollback = function(cb) {
return this.connector.rollback(this.connection, cb);
};
/**
* Begin a new transaction
* @param {Connector} connector The connector instance
* @param {Object} [options] Options {isolationLevel: '...', timeout: 1000}
* @param cb
*/
Transaction.begin = function(connector, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
if (typeof options === 'string') {
options = {isolationLevel: options};
}
var isolationLevel = options.isolationLevel || Transaction.READ_COMMITTED;
assert(isolationLevel === Transaction.SERIALIZABLE ||
isolationLevel === Transaction.REPEATABLE_READ ||
isolationLevel === Transaction.READ_COMMITTED ||
isolationLevel === Transaction.READ_UNCOMMITTED, 'Invalid isolationLevel');
debug('Starting a transaction with options: %j', options);
assert(typeof connector.beginTransaction === 'function',
'beginTransaction must be function implemented by the connector');
connector.beginTransaction(isolationLevel, function(err, connection) {
if (err) {
return cb(err);
}
var tx = connection;
if (!(connection instanceof Transaction)) {
tx = new Transaction(connector, connection);
}
cb(err, tx);
});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 1 1 1 | // Copyright IBM Corp. 2012,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var Promise = require('bluebird');
exports.createPromiseCallback = createPromiseCallback;
function createPromiseCallback() {
var cb;
var promise = new Promise(function(resolve, reject) {
cb = function(err, data) {
if (err) return reject(err);
return resolve(data);
};
});
cb.promise = promise;
return cb;
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| index.js | 85.71% | (12 / 14) | 100% | (0 / 0) | 0% | (0 / 2) | 85.71% | (12 / 14) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2011,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var SG = require('strong-globalize');
SG.SetRootDir(__dirname);
exports.ModelBuilder = exports.LDL = require('./lib/model-builder.js').ModelBuilder;
exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource;
exports.ModelBaseClass = require('./lib/model.js');
exports.GeoPoint = require('./lib/geo.js').GeoPoint;
exports.ValidationError = require('./lib/validations.js').ValidationError;
Object.defineProperty(exports, 'version', {
get: function() { return require('./package.json').version; },
});
var commonTest = './test/common_test';
Object.defineProperty(exports, 'test', {
get: function() { return require(commonTest); },
});
exports.Transaction = require('loopback-connector').Transaction;
exports.KeyValueAccessObject = require('./lib/kvao');
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| dao.js | 7.26% | (119 / 1638) | 0% | (0 / 1065) | 0% | (0 / 201) | 7.63% | (119 / 1560) | |
| datasource.js | 10.37% | (92 / 887) | 0.17% | (1 / 595) | 0% | (0 / 115) | 10.42% | (92 / 883) | |
| geo.js | 12.38% | (13 / 105) | 0% | (0 / 62) | 0% | (0 / 14) | 13.27% | (13 / 98) | |
| hooks.js | 54.55% | (30 / 55) | 26.47% | (9 / 34) | 37.5% | (3 / 8) | 58.82% | (30 / 51) | |
| include.js | 10.38% | (52 / 501) | 0% | (0 / 275) | 0% | (0 / 73) | 10.57% | (52 / 492) | |
| include_utils.js | 15.38% | (8 / 52) | 0% | (0 / 6) | 0% | (0 / 10) | 15.69% | (8 / 51) | |
| introspection.js | 11.76% | (4 / 34) | 0% | (0 / 30) | 50% | (1 / 2) | 11.76% | (4 / 34) | |
| jutil.js | 63.64% | (28 / 44) | 45% | (18 / 40) | 50% | (3 / 6) | 63.64% | (28 / 44) | |
| list.js | 21.15% | (11 / 52) | 0% | (0 / 31) | 0% | (0 / 7) | 21.15% | (11 / 52) | |
| mixins.js | 28.57% | (10 / 35) | 0% | (0 / 18) | 16.67% | (1 / 6) | 28.57% | (10 / 35) | |
| model-builder.js | 63.11% | (207 / 328) | 59.13% | (123 / 208) | 36.67% | (11 / 30) | 63.3% | (207 / 327) | |
| model-definition.js | 50.68% | (75 / 148) | 24.73% | (23 / 93) | 33.33% | (6 / 18) | 50.68% | (75 / 148) | |
| model.js | 34.13% | (100 / 293) | 18.36% | (47 / 256) | 15% | (3 / 20) | 34.13% | (100 / 293) | |
| observer.js | 20.78% | (16 / 77) | 3.7% | (2 / 54) | 6.25% | (1 / 16) | 23.53% | (16 / 68) | |
| relation-definition.js | 7.1% | (130 / 1832) | 0% | (0 / 1184) | 0% | (0 / 229) | 7.25% | (130 / 1792) | |
| relations.js | 61.11% | (11 / 18) | 100% | (0 / 0) | 0% | (0 / 8) | 61.11% | (11 / 18) | |
| scope.js | 7.95% | (21 / 264) | 0% | (0 / 204) | 0% | (0 / 26) | 7.98% | (21 / 263) | |
| transaction.js | 29.23% | (19 / 65) | 4.55% | (1 / 22) | 0% | (0 / 17) | 29.69% | (19 / 64) | |
| types.js | 70.73% | (29 / 41) | 25% | (2 / 8) | 25% | (2 / 8) | 70.73% | (29 / 41) | |
| utils.js | 19.43% | (61 / 314) | 7.73% | (18 / 233) | 9.09% | (3 / 33) | 20.07% | (61 / 304) | |
| validations.js | 21.52% | (65 / 302) | 6.06% | (14 / 231) | 9.09% | (4 / 44) | 23.05% | (65 / 282) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// Turning on strict for this file breaks lots of test cases;
// disabling strict for this file
/* eslint-disable strict */
/*!
* Module exports class Model
*/
module.exports = DataAccessObject;
/*!
* Module dependencies
*/
var g = require('strong-globalize')();
var async = require('async');
var jutil = require('./jutil');
var ValidationError = require('./validations').ValidationError;
var Relation = require('./relations.js');
var Inclusion = require('./include.js');
var List = require('./list.js');
var geo = require('./geo');
var Memory = require('./connectors/memory').Memory;
var utils = require('./utils');
var fieldsToArray = utils.fieldsToArray;
var removeUndefined = utils.removeUndefined;
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var idEquals = utils.idEquals;
var mergeQuery = utils.mergeQuery;
var util = require('util');
var assert = require('assert');
var BaseModel = require('./model');
var debug = require('debug')('loopback:dao');
/**
* Base class for all persistent objects.
* Provides a common API to access any database connector.
* This class describes only abstract behavior. Refer to the specific connector for additional details.
*
* `DataAccessObject` mixes `Inclusion` classes methods.
* @class DataAccessObject
*/
function DataAccessObject() {
if (DataAccessObject._mixins) {
var self = this;
var args = arguments;
DataAccessObject._mixins.forEach(function(m) {
m.call(self, args);
});
}
}
function idName(m) {
return m.definition.idName() || 'id';
}
function getIdValue(m, data) {
return data && data[idName(m)];
}
function copyData(from, to) {
for (var key in from) {
to[key] = from[key];
}
}
function convertSubsetOfPropertiesByType(inst, data) {
var typedData = {};
for (var key in data) {
// Convert the properties by type
typedData[key] = inst[key];
if (typeof typedData[key] === 'object' &&
typedData[key] !== null &&
typeof typedData[key].toObject === 'function') {
typedData[key] = typedData[key].toObject();
}
}
return typedData;
}
/**
* Apply strict check for model's data.
* Notice: Please note this method modifies `inst` when `strict` is `validate`.
*/
function applyStrictCheck(model, strict, data, inst, cb) {
var props = model.definition.properties;
var keys = Object.keys(data);
var result = {}, key;
for (var i = 0; i < keys.length; i++) {
key = keys[i];
if (props[key]) {
result[key] = data[key];
} else if (strict) {
inst.__unknownProperties.push(key);
}
}
cb(null, result);
}
function setIdValue(m, data, value) {
if (data) {
data[idName(m)] = value;
}
}
function byIdQuery(m, id) {
var pk = idName(m);
var query = {where: {}};
query.where[pk] = id;
return query;
}
function isWhereByGivenId(Model, where, idValue) {
var keys = Object.keys(where);
if (keys.length != 1) return false;
var pk = idName(Model);
if (keys[0] !== pk) return false;
return where[pk] === idValue;
}
DataAccessObject._forDB = function(data) {
if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
return data;
}
var res = {};
for (var propName in data) {
var type = this.getPropertyType(propName);
if (type === 'JSON' || type === 'Any' || type === 'Object' || data[propName] instanceof Array) {
res[propName] = JSON.stringify(data[propName]);
} else {
res[propName] = data[propName];
}
}
return res;
};
DataAccessObject.defaultScope = function(target, inst) {
var scope = this.definition.settings.scope;
if (typeof scope === 'function') {
scope = this.definition.settings.scope.call(this, target, inst);
}
return scope;
};
DataAccessObject.applyScope = function(query, inst) {
var scope = this.defaultScope(query, inst) || {};
if (typeof scope === 'object') {
mergeQuery(query, scope || {}, this.definition.settings.scope);
}
};
DataAccessObject.applyProperties = function(data, inst) {
var properties = this.definition.settings.properties;
properties = properties || this.definition.settings.attributes;
if (typeof properties === 'object') {
util._extend(data, properties);
} else if (typeof properties === 'function') {
util._extend(data, properties.call(this, data, inst) || {});
} else if (properties !== false) {
var scope = this.defaultScope(data, inst) || {};
if (typeof scope.where === 'object') {
setScopeValuesFromWhere(data, scope.where, this);
}
}
};
DataAccessObject.lookupModel = function(data) {
return this;
};
/**
* Get the connector instance for the given model class
* @returns {Connector} The connector instance
*/
DataAccessObject.getConnector = function() {
return this.getDataSource().connector;
};
/**
* Verify if allowExtendedOperators is enabled
* @options {Object} [options] Optional options to use.
* @property {Boolean} allowExtendedOperators.
* @returns {Boolean} Returns `true` if allowExtendedOperators is enabled, else `false`.
*/
DataAccessObject._allowExtendedOperators = function(options) {
options = options || {};
var Model = this;
var dsSettings = this.getDataSource().settings;
var allowExtendedOperators = dsSettings.allowExtendedOperators;
// options settings enable allowExtendedOperators per request (for example if
// enable allowExtendedOperators only server side);
// model settings enable allowExtendedOperators only for specific model.
// dataSource settings enable allowExtendedOperators globally (all models);
// options -> model -> dataSource (connector)
if (options.hasOwnProperty('allowExtendedOperators')) {
allowExtendedOperators = options.allowExtendedOperators === true;
} else if (Model.settings && Model.settings.hasOwnProperty('allowExtendedOperators')) {
allowExtendedOperators = Model.settings.allowExtendedOperators === true;
}
return allowExtendedOperators;
};
// Empty callback function
function noCallback(err, result) {
// NOOP
debug('callback is ignored: err=%j, result=%j', err, result);
}
/**
* Create an instance of Model with given data and save to the attached data source. Callback is optional.
* Example:
*```js
* User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
* console.log(user instanceof User); // true
* });
* ```
* Note: You must include a callback and use the created model provided in the callback if your code depends on your model being
* saved or having an ID.
*
* @param {Object} [data] Optional data object
* @param {Object} [options] Options for create
* @param {Function} [cb] Callback function called with these arguments:
* - err (null or Error)
* - instance (null or Model)
*/
DataAccessObject.create = function(data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
var Model = this;
var connector = Model.getConnector();
assert(typeof connector.create === 'function',
'create() must be implemented by the connector');
var self = this;
if (options === undefined && cb === undefined) {
if (typeof data === 'function') {
// create(cb)
cb = data;
data = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// create(data, cb);
cb = options;
options = {};
}
}
data = data || {};
options = options || {};
cb = cb || (Array.isArray(data) ? noCallback : utils.createPromiseCallback());
assert(typeof data === 'object', 'The data argument must be an object or array');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var hookState = {};
if (Array.isArray(data)) {
// Undefined item will be skipped by async.map() which internally uses
// Array.prototype.map(). The following loop makes sure all items are
// iterated
for (var i = 0, n = data.length; i < n; i++) {
if (data[i] === undefined) {
data[i] = {};
}
}
async.map(data, function(item, done) {
self.create(item, options, function(err, result) {
// Collect all errors and results
done(null, {err: err, result: result || item});
});
}, function(err, results) {
if (err) {
return cb(err, results);
}
// Convert the results into two arrays
var errors = null;
var data = [];
for (var i = 0, n = results.length; i < n; i++) {
if (results[i].err) {
if (!errors) {
errors = [];
}
errors[i] = results[i].err;
}
data[i] = results[i].result;
}
cb(errors, data);
});
return;
}
var enforced = {};
var obj;
var idValue = getIdValue(this, data);
// if we come from save
if (data instanceof Model && !idValue) {
obj = data;
} else {
obj = new Model(data);
}
this.applyProperties(enforced, obj);
obj.setAttributes(enforced);
Model = this.lookupModel(data); // data-specific
if (Model !== obj.constructor) obj = new Model(data);
var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
data = obj.toObject(true);
// options has precedence on model-setting
if (options.validate === false) {
return create();
}
// only when options.validate is not set, take model-setting into consideration
if (options.validate === undefined && Model.settings.automaticValidation === false) {
return create();
}
// validation required
obj.isValid(function(valid) {
if (valid) {
create();
} else {
cb(new ValidationError(obj), obj);
}
}, data, options);
});
function create() {
obj.trigger('create', function(createDone) {
obj.trigger('save', function(saveDone) {
var _idName = idName(Model);
var modelName = Model.modelName;
var val = removeUndefined(obj.toObject(true));
function createCallback(err, id, rev) {
if (id) {
obj.__data[_idName] = id;
defineReadonlyProp(obj, _idName, id);
}
if (rev) {
obj._rev = rev;
}
if (err) {
return cb(err, obj);
}
obj.__persisted = true;
var context = {
Model: Model,
data: val,
isNewInstance: true,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('loaded', context, function(err) {
if (err) return cb(err);
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
if (Model.settings.updateOnLoad) {
obj.setAttributes(context.data);
}
saveDone.call(obj, function() {
createDone.call(obj, function() {
if (err) {
return cb(err, obj);
}
var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState,
options: options,
};
if (options.notify !== false) {
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj);
});
} else {
cb(null, obj);
}
});
});
});
}
context = {
Model: Model,
data: val,
isNewInstance: true,
currentInstance: obj,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return cb(err);
if (connector.create.length === 4) {
connector.create(modelName, obj.constructor._forDB(context.data), options, createCallback);
} else {
connector.create(modelName, obj.constructor._forDB(context.data), createCallback);
}
});
}, obj, cb);
}, obj, cb);
}
return cb.promise;
};
function stillConnecting(dataSource, obj, args) {
if (typeof args[args.length - 1] === 'function') {
return dataSource.ready(obj, args);
}
// promise variant
var promiseArgs = Array.prototype.slice.call(args);
promiseArgs.callee = args.callee;
var cb = utils.createPromiseCallback();
promiseArgs.push(cb);
if (dataSource.ready(obj, promiseArgs)) {
return cb.promise;
} else {
return false;
}
}
/**
* Update or insert a model instance: update exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
* otherwise, insert a new record.
*
* NOTE: No setters, validations, or hooks are applied when using upsert.
* `updateOrCreate` and `patchOrCreate` are aliases
* @param {Object} data The model instance data
* @param {Object} [options] Options for upsert
* @param {Function} cb The callback function (optional).
*/
// [FIXME] rfeng: This is a hack to set up 'upsert' first so that
// 'upsert' will be used as the name for strong-remoting to keep it backward
// compatible for angular SDK
DataAccessObject.updateOrCreate =
DataAccessObject.patchOrCreate =
DataAccessObject.upsert = function(data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (options === undefined && cb === undefined) {
if (typeof data === 'function') {
// upsert(cb)
cb = data;
data = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// upsert(data, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
data = data || {};
options = options || {};
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (Array.isArray(data)) {
cb(new Error('updateOrCreate does not support bulk mode or any array input'));
return cb.promise;
}
var hookState = {};
var self = this;
var Model = this;
var connector = Model.getConnector();
var id = getIdValue(this, data);
if (id === undefined || id === null) {
return this.create(data, options, cb);
}
var context = {
Model: Model,
query: byIdQuery(Model, id),
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, doUpdateOrCreate);
function doUpdateOrCreate(err, ctx) {
if (err) return cb(err);
var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id);
if (connector.updateOrCreate && isOriginalQuery) {
var context = {
Model: Model,
where: ctx.query.where,
data: data,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
data = ctx.data;
var update = data;
var inst = data;
if (!(data instanceof Model)) {
inst = new Model(data, {applyDefaultValues: false});
}
update = inst.toObject(false);
Model.applyProperties(update, inst);
Model = Model.lookupModel(update);
var connector = self.getConnector();
var doValidate = undefined;
if (options.validate === undefined) {
if (Model.settings.validateUpsert === undefined) {
if (Model.settings.automaticValidation !== undefined) {
doValidate = Model.settings.automaticValidation;
}
} else {
doValidate = Model.settings.validateUpsert;
}
} else {
doValidate = options.validate;
}
if (doValidate === false) {
callConnector();
} else {
inst.isValid(function(valid) {
if (!valid) {
if (doValidate) { // backwards compatibility with validateUpsert:undefined
return cb(new ValidationError(inst), inst);
} else {
// TODO(bajtos) Remove validateUpsert:undefined in v3.0
g.warn('Ignoring validation errors in {{updateOrCreate()}}:');
g.warn(' %s', new ValidationError(inst).message);
// continue with updateOrCreate
}
}
callConnector();
}, update, options);
}
function callConnector() {
update = removeUndefined(update);
context = {
Model: Model,
where: ctx.where,
data: update,
currentInstance: inst,
hookState: ctx.hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return done(err);
if (connector.updateOrCreate.length === 4) {
connector.updateOrCreate(Model.modelName, update, options, done);
} else {
connector.updateOrCreate(Model.modelName, update, done);
}
});
}
function done(err, data, info) {
if (err) return cb(err);
var context = {
Model: Model,
data: data,
isNewInstance: info && info.isNewInstance,
hookState: ctx.hookState,
options: options,
};
Model.notifyObserversOf('loaded', context, function(err) {
if (err) return cb(err);
var obj;
if (data && !(data instanceof Model)) {
inst._initProperties(data, {persisted: true});
obj = inst;
} else {
obj = data;
}
if (err) {
cb(err, obj);
} else {
var context = {
Model: Model,
instance: obj,
isNewInstance: info ? info.isNewInstance : undefined,
hookState: hookState,
options: options,
};
if (options.notify !== false) {
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj);
});
} else {
cb(null, obj);
}
}
});
}
});
} else {
var opts = {notify: false};
if (ctx.options && ctx.options.transaction) {
opts.transaction = ctx.options.transaction;
}
Model.findOne({where: ctx.query.where}, opts, function(err, inst) {
if (err) {
return cb(err);
}
if (!isOriginalQuery) {
// The custom query returned from a hook may hide the fact that
// there is already a model with `id` value `data[idName(Model)]`
delete data[idName(Model)];
}
if (inst) {
inst.updateAttributes(data, options, cb);
} else {
Model = self.lookupModel(data);
var obj = new Model(data);
obj.save(options, cb);
}
});
}
}
return cb.promise;
};
/**
* Update or insert a model instance based on the search criteria.
* If there is a single instance retrieved, update the retrieved model.
* Creates a new model if no model instances were found.
* Returns an error if multiple instances are found.
* @param {Object} [where] `where` filter, like
* ```
* { key: val, key2: {gt: 'val2'}, ...}
* ```
* <br/>see
* [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods).
* @param {Object} data The model instance data to insert.
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
* @param {Object} model Updated model instance.
*/
DataAccessObject.patchOrCreateWithWhere =
DataAccessObject.upsertWithWhere = function(where, data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) { return connectionPromise; }
if (cb === undefined) {
if (typeof options === 'function') {
// upsertWithWhere(where, data, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
options = options || {};
assert(typeof where === 'object', 'The where argument must be an object');
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (Object.keys(data).length === 0) {
var err = new Error('data object cannot be empty!');
err.statusCode = 400;
process.nextTick(function() { cb(err); });
return cb.promise;
}
var hookState = {};
var self = this;
var Model = this;
var connector = Model.getConnector();
var modelName = Model.modelName;
var query = {where: where};
var context = {
Model: Model,
query: query,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, doUpsertWithWhere);
function doUpsertWithWhere(err, ctx) {
if (err) return cb(err);
ctx.data = data;
if (connector.upsertWithWhere) {
var context = {
Model: Model,
where: ctx.query.where,
data: ctx.data,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
data = ctx.data;
var update = data;
var inst = data;
if (!(data instanceof Model)) {
inst = new Model(data, {applyDefaultValues: false});
}
update = inst.toObject(false);
Model.applyScope(query);
Model.applyProperties(update, inst);
Model = Model.lookupModel(update);
if (options.validate === false) {
return callConnector();
}
if (options.validate === undefined && Model.settings.automaticValidation === false) {
return callConnector();
}
inst.isValid(function(valid) {
if (!valid) return cb(new ValidationError(inst), inst);
callConnector();
}, update, options);
function callConnector() {
try {
ctx.where = removeUndefined(ctx.where);
ctx.where = Model._coerce(ctx.where, options);
update = removeUndefined(update);
update = Model._coerce(update, options);
} catch (err) {
return process.nextTick(function() {
cb(err);
});
}
context = {
Model: Model,
where: ctx.where,
data: update,
currentInstance: inst,
hookState: ctx.hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return done(err);
connector.upsertWithWhere(modelName, ctx.where, update, options, done);
});
}
function done(err, data, info) {
if (err) return cb(err);
var contxt = {
Model: Model,
data: data,
isNewInstance: info && info.isNewInstance,
hookState: ctx.hookState,
options: options,
};
Model.notifyObserversOf('loaded', contxt, function(err) {
if (err) return cb(err);
var obj;
if (contxt.data && !(contxt.data instanceof Model)) {
inst._initProperties(contxt.data, {persisted: true});
obj = inst;
} else {
obj = contxt.data;
}
var context = {
Model: Model,
instance: obj,
isNewInstance: info ? info.isNewInstance : undefined,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj);
});
});
}
});
} else {
var opts = {notify: false};
if (ctx.options && ctx.options.transaction) {
opts.transaction = ctx.options.transaction;
}
self.find({where: ctx.query.where}, opts, function(err, instances) {
if (err) return cb(err);
var modelsLength = instances.length;
if (modelsLength === 0) {
self.create(data, options, cb);
} else if (modelsLength === 1) {
var modelInst = instances[0];
modelInst.updateAttributes(data, options, cb);
} else {
process.nextTick(function() {
var error = new Error('There are multiple instances found.' +
'Upsert Operation will not be performed!');
error.statusCode = 400;
cb(error);
});
}
});
}
}
return cb.promise;
};
/**
* Replace or insert a model instance: replace exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
* otherwise, insert a new record.
*
* @param {Object} data The model instance data
* @param {Object} [options] Options for replaceOrCreate
* @param {Function} cb The callback function (optional).
*/
DataAccessObject.replaceOrCreate = function replaceOrCreate(data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (cb === undefined) {
if (typeof options === 'function') {
// replaceOrCreta(data,cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
data = data || {};
options = options || {};
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var hookState = {};
var self = this;
var Model = this;
var connector = Model.getConnector();
var id = getIdValue(this, data);
if (id === undefined || id === null) {
return this.create(data, options, cb);
}
var forceId = this.settings.forceId;
if (forceId) {
return Model.replaceById(id, data, options, cb);
}
var inst;
if (data instanceof Model) {
inst = data;
} else {
inst = new Model(data);
}
var strict = inst.__strict;
var context = {
Model: Model,
query: byIdQuery(Model, id),
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, doReplaceOrCreate);
function doReplaceOrCreate(err, ctx) {
if (err) return cb(err);
var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id);
var where = ctx.query.where;
if (connector.replaceOrCreate && isOriginalQuery) {
var context = {
Model: Model,
instance: inst,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
var update = inst.toObject(false);
if (strict) {
applyStrictCheck(Model, strict, update, inst, validateAndCallConnector);
} else {
validateAndCallConnector();
}
function validateAndCallConnector(err) {
if (err) return cb(err);
Model.applyProperties(update, inst);
Model = Model.lookupModel(update);
var connector = self.getConnector();
if (options.validate === false) {
return callConnector();
}
// only when options.validate is not set, take model-setting into consideration
if (options.validate === undefined && Model.settings.automaticValidation === false) {
return callConnector();
}
inst.isValid(function(valid) {
if (!valid) return cb(new ValidationError(inst), inst);
callConnector();
}, update, options);
function callConnector() {
update = removeUndefined(update);
context = {
Model: Model,
where: where,
data: update,
currentInstance: inst,
hookState: ctx.hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return done(err);
connector.replaceOrCreate(Model.modelName, context.data, options, done);
});
}
function done(err, data, info) {
if (err) return cb(err);
var context = {
Model: Model,
data: data,
isNewInstance: info ? info.isNewInstance : undefined,
hookState: ctx.hookState,
options: options,
};
Model.notifyObserversOf('loaded', context, function(err) {
if (err) return cb(err);
var obj;
if (data && !(data instanceof Model)) {
inst._initProperties(data, {persisted: true});
obj = inst;
} else {
obj = data;
}
if (err) {
cb(err, obj);
} else {
var context = {
Model: Model,
instance: obj,
isNewInstance: info ? info.isNewInstance : undefined,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj, info);
});
}
});
}
}
});
} else {
var opts = {notify: false};
if (ctx.options && ctx.options.transaction) {
opts.transaction = ctx.options.transaction;
}
Model.findOne({where: ctx.query.where}, opts, function(err, found) {
if (err) return cb(err);
if (!isOriginalQuery) {
// The custom query returned from a hook may hide the fact that
// there is already a model with `id` value `data[idName(Model)]`
var pkName = idName(Model);
delete data[pkName];
if (found) id = found[pkName];
}
if (found) {
self.replaceById(id, data, options, cb);
} else {
Model = self.lookupModel(data);
var obj = new Model(data);
obj.save(options, cb);
}
});
}
}
return cb.promise;
};
/**
* Find one record that matches specified query criteria. Same as `find`, but limited to one record, and this function returns an
* object, not a collection.
* If the specified instance is not found, then create it using data provided as second argument.
*
* @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format.
* For example: `{where: {test: 'me'}}`.
* @param {Object} data Object to create.
* @param {Object} [options] Option for findOrCreate
* @param {Function} cb Callback called with (err, instance, created)
*/
DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
assert(arguments.length >= 1, 'At least one argument is required');
if (data === undefined && options === undefined && cb === undefined) {
assert(typeof query === 'object', 'Single argument must be data object');
// findOrCreate(data);
// query will be built from data, and method will return Promise
data = query;
query = {where: data};
} else if (options === undefined && cb === undefined) {
if (typeof data === 'function') {
// findOrCreate(data, cb);
// query will be built from data
cb = data;
data = query;
query = {where: data};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// findOrCreate(query, data, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
query = query || {where: {}};
data = data || {};
options = options || {};
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var hookState = {};
var Model = this;
var self = this;
var connector = Model.getConnector();
function _findOrCreate(query, data, currentInstance) {
var modelName = self.modelName;
function findOrCreateCallback(err, data, created) {
if (err) return cb(err);
var context = {
Model: Model,
data: data,
isNewInstance: created,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('loaded', context, function(err) {
if (err) return cb(err);
var obj, Model = self.lookupModel(data);
if (data) {
obj = new Model(data, {fields: query.fields, applySetters: false,
persisted: true});
}
if (created) {
var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('after save', context, function(err) {
if (cb.promise) {
cb(err, [obj, created]);
} else {
cb(err, obj, created);
}
});
} else {
if (cb.promise) {
cb(err, [obj, created]);
} else {
cb(err, obj, created);
}
}
});
}
data = removeUndefined(data);
var context = {
Model: Model,
where: query.where,
data: data,
isNewInstance: true,
currentInstance: currentInstance,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return cb(err);
if (connector.findOrCreate.length === 5) {
connector.findOrCreate(modelName, query, self._forDB(context.data), options, findOrCreateCallback);
} else {
connector.findOrCreate(modelName, query, self._forDB(context.data), findOrCreateCallback);
}
});
}
if (connector.findOrCreate) {
query.limit = 1;
try {
this._normalize(query, options);
} catch (err) {
process.nextTick(function() {
cb(err);
});
return cb.promise;
}
this.applyScope(query);
var context = {
Model: Model,
query: query,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
var query = ctx.query;
var enforced = {};
var Model = self.lookupModel(data);
var obj = data instanceof Model ? data : new Model(data);
Model.applyProperties(enforced, obj);
obj.setAttributes(enforced);
var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
var obj = ctx.instance;
var data = obj.toObject(true);
// options has precedence on model-setting
if (options.validate === false) {
return _findOrCreate(query, data, obj);
}
// only when options.validate is not set, take model-setting into consideration
if (options.validate === undefined && Model.settings.automaticValidation === false) {
return _findOrCreate(query, data, obj);
}
// validation required
obj.isValid(function(valid) {
if (valid) {
_findOrCreate(query, data, obj);
} else {
cb(new ValidationError(obj), obj);
}
}, data, options);
});
});
} else {
Model.findOne(query, options, function(err, record) {
if (err) return cb(err);
if (record) {
if (cb.promise) {
return cb(null, [record, false]);
} else {
return cb(null, record, false);
}
}
Model.create(data, options, function(err, record) {
if (cb.promise) {
cb(err, [record, record != null]);
} else {
cb(err, record, record != null);
}
});
});
}
return cb.promise;
};
/**
* Check whether a model instance exists in database
*
* @param {id} id Identifier of object (primary key value)
* @param {Object} [options] Options
* @param {Function} cb Callback function called with (err, exists: Bool)
*/
DataAccessObject.exists = function exists(id, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
assert(arguments.length >= 1, 'The id argument is required');
if (cb === undefined) {
if (typeof options === 'function') {
// exists(id, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
options = options || {};
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (id !== undefined && id !== null && id !== '') {
this.count(byIdQuery(this, id).where, options, function(err, count) {
cb(err, err ? false : count === 1);
});
} else {
process.nextTick(function() {
cb(new Error(g.f('{{Model::exists}} requires the {{id}} argument')));
});
}
return cb.promise;
};
/**
* Find model instance by ID.
*
* Example:
* ```js
* User.findById(23, function(err, user) {
* console.info(user.id); // 23
* });
* ```
*
* @param {*} id Primary key value
* @param {Object} [filter] The filter that contains `include` or `fields`.
* Other settings such as `where`, `order`, `limit`, or `offset` will be
* ignored.
* @param {Object} [options] Options
* @param {Function} cb Callback called with (err, instance)
*/
DataAccessObject.findById = function findById(id, filter, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
assert(arguments.length >= 1, 'The id argument is required');
if (options === undefined && cb === undefined) {
if (typeof filter === 'function') {
// findById(id, cb)
cb = filter;
filter = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// findById(id, query, cb)
cb = options;
options = {};
if (typeof filter === 'object' && !(filter.include || filter.fields)) {
// If filter doesn't have include or fields, assuming it's options
options = filter;
filter = {};
}
}
}
cb = cb || utils.createPromiseCallback();
options = options || {};
filter = filter || {};
assert(typeof filter === 'object', 'The filter argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (isPKMissing(this, cb)) {
return cb.promise;
} else if (id == null || id === '') {
process.nextTick(function() {
cb(new Error(g.f('{{Model::findById}} requires the {{id}} argument')));
});
} else {
var query = byIdQuery(this, id);
if (filter.include) {
query.include = filter.include;
}
if (filter.fields) {
query.fields = filter.fields;
}
this.findOne(query, options, cb);
}
return cb.promise;
};
/**
* Find model instances by ids
* @param {Array} ids An array of ids
* @param {Object} query Query filter
* @param {Object} [options] Options
* @param {Function} cb Callback called with (err, instance)
*/
DataAccessObject.findByIds = function(ids, query, options, cb) {
if (options === undefined && cb === undefined) {
if (typeof query === 'function') {
// findByIds(ids, cb)
cb = query;
query = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// findByIds(ids, query, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
options = options || {};
query = query || {};
assert(Array.isArray(ids), 'The ids argument must be an array');
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (isPKMissing(this, cb)) {
return cb.promise;
} else if (ids.length === 0) {
process.nextTick(function() { cb(null, []); });
return cb.promise;
}
var filter = {where: {}};
var pk = idName(this);
filter.where[pk] = {inq: [].concat(ids)};
mergeQuery(filter, query || {});
// to know if the result need to be sorted by ids or not
// this variable need to be initialized before the call to find, because filter is updated during the call with an order
var toSortObjectsByIds = filter.order ? false : true;
this.find(filter, options, function(err, results) {
cb(err, toSortObjectsByIds ? utils.sortObjectsByIds(pk, ids, results) : results);
});
return cb.promise;
};
function convertNullToNotFoundError(ctx, cb) {
if (ctx.result !== null) return cb();
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg);
error.statusCode = error.status = 404;
cb(error);
}
// alias function for backwards compat.
DataAccessObject.all = function() {
return DataAccessObject.find.apply(this, arguments);
};
/**
* Get settings via hiarchical determiniation
*
* @param {String} key The setting key
*/
DataAccessObject._getSetting = function(key) {
// Check for settings in model
var m = this.definition;
if (m && m.settings && m.settings[key]) {
return m.settings[key];
}
// Check for settings in connector
var ds = this.getDataSource();
if (ds && ds.settings && ds.settings[key]) {
return ds.settings[key];
}
return;
};
var operators = {
gt: '>',
gte: '>=',
lt: '<',
lte: '<=',
between: 'BETWEEN',
inq: 'IN',
nin: 'NOT IN',
neq: '!=',
like: 'LIKE',
nlike: 'NOT LIKE',
ilike: 'ILIKE',
nilike: 'NOT ILIKE',
regexp: 'REGEXP',
};
/*
* Normalize the filter object and throw errors if invalid values are detected
* @param {Object} filter The query filter object
* @options {Object} [options] Optional options to use.
* @property {Boolean} allowExtendedOperators.
* @returns {Object} The normalized filter object
* @private
*/
DataAccessObject._normalize = function(filter, options) {
if (!filter) {
return undefined;
}
var err = null;
if ((typeof filter !== 'object') || Array.isArray(filter)) {
err = new Error(g.f('The query filter %j is not an {{object}}', filter));
err.statusCode = 400;
throw err;
}
if (filter.limit || filter.skip || filter.offset) {
var limit = Number(filter.limit || 100);
var offset = Number(filter.skip || filter.offset || 0);
if (isNaN(limit) || limit <= 0 || Math.ceil(limit) !== limit) {
err = new Error(g.f('The {{limit}} parameter %j is not valid',
filter.limit));
err.statusCode = 400;
throw err;
}
if (isNaN(offset) || offset < 0 || Math.ceil(offset) !== offset) {
err = new Error(g.f('The {{offset/skip}} parameter %j is not valid',
filter.skip || filter.offset));
err.statusCode = 400;
throw err;
}
filter.limit = limit;
filter.offset = offset;
filter.skip = offset;
}
if (filter.order) {
var order = filter.order;
if (!Array.isArray(order)) {
order = [order];
}
var fields = [];
for (var i = 0, m = order.length; i < m; i++) {
if (typeof order[i] === 'string') {
// Normalize 'f1 ASC, f2 DESC, f3' to ['f1 ASC', 'f2 DESC', 'f3']
var tokens = order[i].split(/(?:\s*,\s*)+/);
for (var t = 0, n = tokens.length; t < n; t++) {
var token = tokens[t];
if (token.length === 0) {
// Skip empty token
continue;
}
var parts = token.split(/\s+/);
if (parts.length >= 2) {
var dir = parts[1].toUpperCase();
if (dir === 'ASC' || dir === 'DESC') {
token = parts[0] + ' ' + dir;
} else {
err = new Error(g.f('The {{order}} %j has invalid direction', token));
err.statusCode = 400;
throw err;
}
}
fields.push(token);
}
} else {
err = new Error(g.f('The order %j is not valid', order[i]));
err.statusCode = 400;
throw err;
}
}
if (fields.length === 1 && typeof filter.order === 'string') {
filter.order = fields[0];
} else {
filter.order = fields;
}
}
// normalize fields as array of included property names
if (filter.fields) {
filter.fields = fieldsToArray(filter.fields,
Object.keys(this.definition.properties), this.settings.strict);
}
var handleUndefined = this._getSetting('normalizeUndefinedInQuery');
// alter configuration of how removeUndefined handles undefined values
filter = removeUndefined(filter, handleUndefined);
this._coerce(filter.where, options);
return filter;
};
function DateType(arg) {
var d = new Date(arg);
if (isNaN(d.getTime())) {
throw new Error(g.f('Invalid date: %s', arg));
}
return d;
}
function BooleanType(arg) {
if (typeof arg === 'string') {
switch (arg) {
case 'true':
case '1':
return true;
case 'false':
case '0':
return false;
}
}
if (arg == null) {
return null;
}
return Boolean(arg);
}
function NumberType(val) {
var num = Number(val);
return !isNaN(num) ? num : val;
}
function coerceArray(val) {
if (Array.isArray(val)) {
return val;
}
if (!utils.isPlainObject(val)) {
throw new Error(g.f('Value is not an {{array}} or {{object}} with sequential numeric indices'));
}
// It is an object, check if empty
var props = Object.keys(val);
if (props.length === 0) {
throw new Error(g.f('Value is an empty {{object}}'));
}
var arrayVal = new Array(props.length);
for (var i = 0; i < arrayVal.length; ++i) {
if (!val.hasOwnProperty(i)) {
throw new Error(g.f('Value is not an {{array}} or {{object}} with sequential numeric indices'));
}
arrayVal[i] = val[i];
}
return arrayVal;
}
/*
* Coerce values based the property types
* @param {Object} where The where clause
* @options {Object} [options] Optional options to use.
* @property {Boolean} allowExtendedOperators.
* @returns {Object} The coerced where clause
* @private
*/
DataAccessObject._coerce = function(where, options) {
var self = this;
if (!where) {
return where;
}
options = options || {};
var err;
if (typeof where !== 'object' || Array.isArray(where)) {
err = new Error(g.f('The where clause %j is not an {{object}}', where));
err.statusCode = 400;
throw err;
}
var props = self.definition.properties;
for (var p in where) {
// Handle logical operators
if (p === 'and' || p === 'or' || p === 'nor') {
var clauses = where[p];
try {
clauses = coerceArray(clauses);
} catch (e) {
err = new Error(g.f('The %s operator has invalid clauses %j: %s', p, clauses, e.message));
err.statusCode = 400;
throw err;
}
for (var k = 0; k < clauses.length; k++) {
self._coerce(clauses[k], options);
}
continue;
}
var DataType = props[p] && props[p].type;
if (!DataType) {
continue;
}
if (Array.isArray(DataType) || DataType === Array) {
DataType = DataType[0];
}
if (DataType === Date) {
DataType = DateType;
} else if (DataType === Boolean) {
DataType = BooleanType;
} else if (DataType === Number) {
// This fixes a regression in mongodb connector
// For numbers, only convert it produces a valid number
// LoopBack by default injects a number id. We should fix it based
// on the connector's input, for example, MongoDB should use string
// while RDBs typically use number
DataType = NumberType;
}
if (!DataType) {
continue;
}
if (DataType.prototype instanceof BaseModel) {
continue;
}
if (DataType === geo.GeoPoint) {
// Skip the GeoPoint as the near operator breaks the assumption that
// an operation has only one property
// We should probably fix it based on
// http://docs.mongodb.org/manual/reference/operator/query/near/
// The other option is to make operators start with $
continue;
}
var val = where[p];
if (val === null || val === undefined) {
continue;
}
// Check there is an operator
var operator = null;
var exp = val;
if (val.constructor === Object) {
for (var op in operators) {
if (op in val) {
val = val[op];
operator = op;
switch (operator) {
case 'inq':
case 'nin':
case 'between':
try {
val = coerceArray(val);
} catch (e) {
err = new Error(g.f('The %s property has invalid clause %j: %s', p, where[p], e));
err.statusCode = 400;
throw err;
}
if (operator === 'between' && val.length !== 2) {
err = new Error(g.f(
'The %s property has invalid clause %j: Expected precisely 2 values, received %d',
p,
where[p],
val.length));
err.statusCode = 400;
throw err;
}
break;
case 'like':
case 'nlike':
case 'ilike':
case 'nilike':
if (!(typeof val === 'string' || val instanceof RegExp)) {
err = new Error(g.f(
'The %s property has invalid clause %j: Expected a string or RegExp',
p,
where[p]));
err.statusCode = 400;
throw err;
}
break;
case 'regexp':
val = utils.toRegExp(val);
if (val instanceof Error) {
val.statusCode = 400;
throw err;
}
break;
}
break;
}
}
}
try {
// Coerce val into an array if it resembles an array-like object
val = coerceArray(val);
} catch (e) {
// NOOP when not coercable into an array.
}
// Coerce the array items
if (Array.isArray(val)) {
for (var i = 0; i < val.length; i++) {
if (val[i] !== null && val[i] !== undefined) {
if (!(val[i] instanceof RegExp)) {
val[i] = DataType(val[i]);
}
}
}
} else {
if (val != null) {
const allowExtendedOperators = self._allowExtendedOperators(options);
if (operator === null && val instanceof RegExp) {
// Normalize {name: /A/} to {name: {regexp: /A/}}
operator = 'regexp';
} else if (operator === 'regexp' && val instanceof RegExp) {
// Do not coerce regex literals/objects
} else if ((operator === 'like' || operator === 'nlike' ||
operator === 'ilike' || operator === 'nilike') && val instanceof RegExp) {
// Do not coerce RegExp operator value
} else if (allowExtendedOperators && typeof val === 'object') {
// Do not coerce object values when extended operators are allowed
} else {
val = DataType(val);
}
}
}
// Rebuild {property: {operator: value}}
if (operator) {
var value = {};
value[operator] = val;
if (exp.options) {
// Keep options for operators
value.options = exp.options;
}
val = value;
}
where[p] = val;
}
return where;
};
/**
* Find all instances of Model that match the specified query.
* Fields used for filter and sort should be declared with `{index: true}` in model definition.
* See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information.
*
* For example, find the second page of ten users over age 21 in descending order exluding the password property.
*
* ```js
* User.find({
* where: {
* age: {gt: 21}},
* order: 'age DESC',
* limit: 10,
* skip: 10,
* fields: {password: false}
* },
* console.log
* );
* ```
*
* @options {Object} [query] Optional JSON object that specifies query criteria and parameters.
* @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`.
* Operations:
* - gt: >
* - gte: >=
* - lt: <
* - lte: <=
* - between
* - inq: IN
* - nin: NOT IN
* - neq: !=
* - like: LIKE
* - nlike: NOT LIKE
* - ilike: ILIKE
* - nilike: NOT ILIKE
* - regexp: REGEXP
*
* You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information.
* @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests.
* Format examples;
* - `'posts'`: Load posts
* - `['posts', 'passports']`: Load posts and passports
* - `{'owner': 'posts'}`: Load owner and owner's posts
* - `{'owner': ['posts', 'passports']}`: Load owner, owner's posts, and owner's passports
* - `{'owner': [{posts: 'images'}, 'passports']}`: Load owner, owner's posts, owner's posts' images, and owner's passports
* See `DataAccessObject.include()`.
* @property {String} order Sort order. Format: `'key1 ASC, key2 DESC'`
* @property {Number} limit Maximum number of instances to return.
* @property {Number} skip Number of instances to skip.
* @property {Number} offset Alias for `skip`.
* @property {Object|Array|String} fields Included/excluded fields.
* - `['foo']` or `'foo'` - include only the foo property
* - `['foo', 'bar']` - include the foo and bar properties. Format:
* - `{foo: true}` - include only foo
* - `{bat: false}` - include all properties, exclude bat
*
* @param {Function} cb Optional callback function. Call this function with two arguments: `err` (null or Error) and an array of instances.
* @return {Promise} results If no callback function is provided, a promise (which resolves to an array of instances) is returned
*/
DataAccessObject.find = function find(query, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (options === undefined && cb === undefined) {
if (typeof query === 'function') {
// find(cb);
cb = query;
query = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// find(query, cb);
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
query = query || {};
options = options || {};
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var hookState = {};
var self = this;
var connector = self.getConnector();
assert(typeof connector.all === 'function',
'all() must be implemented by the connector');
try {
this._normalize(query, options);
} catch (err) {
process.nextTick(function() {
cb(err);
});
return cb.promise;
}
this.applyScope(query);
var near = query && geo.nearFilter(query.where);
var supportsGeo = !!connector.buildNearFilter;
if (near) {
if (supportsGeo) {
// convert it
connector.buildNearFilter(query, near);
} else if (query.where) {
// do in memory query
// using all documents
// TODO [fabien] use default scope here?
if (options.notify === false) {
queryGeo(query);
} else {
withNotifyGeo();
}
function withNotifyGeo() {
var context = {
Model: self,
query: query,
hookState: hookState,
options: options,
};
self.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
queryGeo(ctx.query);
});
}
function queryGeo(query) {
function geoCallbackWithoutNotify(err, data) {
var memory = new Memory();
var modelName = self.modelName;
if (err) {
cb(err);
} else if (Array.isArray(data)) {
memory.define({
properties: self.dataSource.definitions[self.modelName].properties,
settings: self.dataSource.definitions[self.modelName].settings,
model: self,
});
data.forEach(function(obj) {
memory.create(modelName, obj, options, function() {
// noop
});
});
// FIXME: apply "includes" and other transforms - see allCb below
memory.all(modelName, query, options, cb);
} else {
cb(null, []);
}
}
function geoCallbackWithNotify(err, data) {
if (err) return cb(err);
async.map(data, function(item, next) {
var context = {
Model: self,
data: item,
isNewInstance: false,
hookState: hookState,
options: options,
};
self.notifyObserversOf('loaded', context, function(err) {
if (err) return next(err);
next(null, context.data);
});
}, function(err, results) {
if (err) return cb(err);
geoCallbackWithoutNotify(null, results);
});
}
var geoCallback = options.notify === false ? geoCallbackWithoutNotify : geoCallbackWithNotify;
if (connector.all.length === 4) {
connector.all(self.modelName, {}, options, geoCallback);
} else {
connector.all(self.modelName, {}, geoCallback);
}
}
// already handled
return cb.promise;
}
}
var allCb = function(err, data) {
if (!err && Array.isArray(data)) {
async.map(data, function(item, next) {
var Model = self.lookupModel(item);
if (options.notify === false) {
buildResult(item, next);
} else {
withNotify(item, next);
}
function buildResult(data, callback) {
var ctorOpts = {
fields: query.fields,
applySetters: false,
persisted: true,
};
var obj;
try {
obj = new Model(data, ctorOpts);
} catch (err) {
return callback(err);
}
if (query && query.include) {
if (query.collect) {
// The collect property indicates that the query is to return the
// standalone items for a related model, not as child of the parent object
// For example, article.tags
obj = obj.__cachedRelations[query.collect];
if (obj === null) {
obj = undefined;
}
} else {
// This handles the case to return parent items including the related
// models. For example, Article.find({include: 'tags'}, ...);
// Try to normalize the include
var includes = Inclusion.normalizeInclude(query.include || []);
includes.forEach(function(inc) {
var relationName = inc;
if (utils.isPlainObject(inc)) {
relationName = Object.keys(inc)[0];
}
// Promote the included model as a direct property
var included = obj.__cachedRelations[relationName];
if (Array.isArray(included)) {
included = new List(included, null, obj);
}
if (included) obj.__data[relationName] = included;
});
delete obj.__data.__cachedRelations;
}
}
callback(null, obj);
}
function withNotify(data, callback) {
var context = {
Model: Model,
data: data,
isNewInstance: false,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('loaded', context, function(err) {
if (err) return callback(err);
buildResult(context.data, callback);
});
}
},
function(err, results) {
if (err) return cb(err);
// When applying query.collect, some root items may not have
// any related/linked item. We store `undefined` in the results
// array in such case, which is not desirable from API consumer's
// point of view.
results = results.filter(isDefined);
if (data && data.countBeforeLimit) {
results.countBeforeLimit = data.countBeforeLimit;
}
if (!supportsGeo && near) {
results = geo.filter(results, near);
}
cb(err, results);
});
} else {
cb(err, data || []);
}
};
if (options.notify === false) {
if (connector.all.length === 4) {
connector.all(self.modelName, query, options, allCb);
} else {
connector.all(self.modelName, query, allCb);
}
} else {
var context = {
Model: this,
query: query,
hookState: hookState,
options: options,
};
this.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
connector.all.length === 4 ?
connector.all(self.modelName, ctx.query, options, allCb) :
connector.all(self.modelName, ctx.query, allCb);
});
}
return cb.promise;
};
function isDefined(value) {
return value !== undefined;
}
/**
* Find one record, same as `find`, but limited to one result. This function returns an object, not a collection.
*
* @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format.
* For example: `{where: {test: 'me'}}`.
* @param {Object} [options] Options
* @param {Function} cb Callback function called with (err, instance)
*/
DataAccessObject.findOne = function findOne(query, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (options === undefined && cb === undefined) {
if (typeof query === 'function') {
cb = query;
query = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
query = query || {};
options = options || {};
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
query.limit = 1;
this.find(query, options, function(err, collection) {
if (err || !collection || !collection.length > 0) return cb(err, null);
cb(err, collection[0]);
});
return cb.promise;
};
/**
* Destroy all matching records.
* Delete all model instances from data source. Note: destroyAll method does not destroy hooks.
* Example:
*````js
* Product.destroyAll({price: {gt: 99}}, function(err) {
// removed matching products
* });
* ````
*
* @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object.
* @param {Object) [options] Options
* @param {Function} [cb] Callback called with (err, info)
*/
DataAccessObject.remove =
DataAccessObject.deleteAll =
DataAccessObject.destroyAll = function destroyAll(where, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
var Model = this;
var connector = Model.getConnector();
assert(typeof connector.destroyAll === 'function',
'destroyAll() must be implemented by the connector');
if (options === undefined && cb === undefined) {
if (typeof where === 'function') {
cb = where;
where = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
where = where || {};
options = options || {};
assert(typeof where === 'object', 'The where argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var hookState = {};
var query = {where: where};
this.applyScope(query);
where = query.where;
if (options.notify === false) {
doDelete(where);
} else {
query = {where: whereIsEmpty(where) ? {} : where};
var context = {
Model: Model,
query: query,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
var context = {
Model: Model,
where: ctx.query.where,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before delete', context, function(err, ctx) {
if (err) return cb(err);
doDelete(ctx.where);
});
});
}
function doDelete(where) {
var context = {
Model: Model,
where: whereIsEmpty(where) ? {} : where,
hookState: hookState,
options: options,
};
if (whereIsEmpty(where)) {
if (connector.destroyAll.length === 4) {
connector.destroyAll(Model.modelName, {}, options, done);
} else {
connector.destroyAll(Model.modelName, {}, done);
}
} else {
try {
// Support an optional where object
where = removeUndefined(where);
where = Model._coerce(where, options);
} catch (err) {
return process.nextTick(function() {
cb(err);
});
}
if (connector.destroyAll.length === 4) {
connector.destroyAll(Model.modelName, where, options, done);
} else {
connector.destroyAll(Model.modelName, where, done);
}
}
function done(err, info) {
if (err) return cb(err);
if (options.notify === false) {
return cb(err, info);
}
var context = {
Model: Model,
where: where,
hookState: hookState,
options: options,
info: info,
};
Model.notifyObserversOf('after delete', context, function(err) {
cb(err, info);
});
}
}
return cb.promise;
};
function whereIsEmpty(where) {
return !where ||
(typeof where === 'object' && Object.keys(where).length === 0);
}
/**
* Delete the record with the specified ID.
* Aliases are `destroyById` and `deleteById`.
* @param {*} id The id value
* @param {Function} cb Callback called with (err)
*/
// [FIXME] rfeng: This is a hack to set up 'deleteById' first so that
// 'deleteById' will be used as the name for strong-remoting to keep it backward
// compatible for angular SDK
DataAccessObject.removeById =
DataAccessObject.destroyById =
DataAccessObject.deleteById = function deleteById(id, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
assert(arguments.length >= 1, 'The id argument is required');
if (cb === undefined) {
if (typeof options === 'function') {
// destroyById(id, cb)
cb = options;
options = {};
}
}
options = options || {};
cb = cb || utils.createPromiseCallback();
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (isPKMissing(this, cb)) {
return cb.promise;
} else if (id == null || id === '') {
process.nextTick(function() {
cb(new Error(g.f('{{Model::deleteById}} requires the {{id}} argument')));
});
return cb.promise;
}
var Model = this;
this.remove(byIdQuery(this, id).where, options, function(err, info) {
if (err) return cb(err);
var deleted = info && info.count > 0;
if (Model.settings.strictDelete && !deleted) {
err = new Error(g.f('No instance with {{id}} %s found for %s', id, Model.modelName));
err.code = 'NOT_FOUND';
err.statusCode = 404;
return cb(err);
}
cb(null, info);
});
return cb.promise;
};
/**
* Return count of matched records. Optional query parameter allows you to count filtered set of model instances.
* Example:
*
*```js
* User.count({approved: true}, function(err, count) {
* console.log(count); // 2081
* });
* ```
*
* @param {Object} [where] Search conditions (optional)
* @param {Object} [options] Options
* @param {Function} cb Callback, called with (err, count)
*/
DataAccessObject.count = function(where, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (options === undefined && cb === undefined) {
if (typeof where === 'function') {
// count(cb)
cb = where;
where = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// count(where, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
where = where || {};
options = options || {};
assert(typeof where === 'object', 'The where argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var Model = this;
var connector = Model.getConnector();
assert(typeof connector.count === 'function',
'count() must be implemented by the connector');
assert(connector.count.length >= 3,
'count() must take at least 3 arguments');
var hookState = {};
var query = {where: where};
this.applyScope(query);
where = query.where;
try {
where = removeUndefined(where);
where = this._coerce(where, options);
} catch (err) {
process.nextTick(function() {
cb(err);
});
return cb.promise;
}
var context = {
Model: Model,
query: {where: where},
hookState: hookState,
options: options,
};
this.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
where = ctx.query.where;
if (connector.count.length <= 3) {
// Old signature, please note where is the last
// count(model, cb, where)
connector.count(Model.modelName, cb, where);
} else {
// New signature
// count(model, where, options, cb)
connector.count(Model.modelName, where, options, cb);
}
});
return cb.promise;
};
/**
* Save instance. If the instance does not have an ID, call `create` instead.
* Triggers: validate, save, update or create.
* @options {Object} options Optional options to use.
* @property {Boolean} validate Default is true.
* @property {Boolean} throws Default is false.
* @param {Function} cb Callback function with err and object arguments
*/
DataAccessObject.prototype.save = function(options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
var Model = this.constructor;
if (typeof options === 'function') {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
options = options || {};
assert(typeof options === 'object', 'The options argument should be an object');
assert(typeof cb === 'function', 'The cb argument should be a function');
if (isPKMissing(Model, cb)) {
return cb.promise;
} else if (this.isNewRecord()) {
return Model.create(this, options, cb);
}
var hookState = {};
if (options.validate === undefined) {
if (Model.settings.automaticValidation === undefined) {
options.validate = true;
} else {
options.validate = Model.settings.automaticValidation;
}
}
if (options.throws === undefined) {
options.throws = false;
}
var inst = this;
var connector = inst.getConnector();
var modelName = Model.modelName;
var context = {
Model: Model,
instance: inst,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
var data = inst.toObject(true);
Model.applyProperties(data, inst);
inst.setAttributes(data);
// validate first
if (!options.validate) {
return save();
}
inst.isValid(function(valid) {
if (valid) {
save();
} else {
var err = new ValidationError(inst);
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
cb(err, inst);
}
}, data, options);
// then save
function save() {
inst.trigger('save', function(saveDone) {
inst.trigger('update', function(updateDone) {
data = removeUndefined(data);
function saveCallback(err, unusedData, result) {
if (err) {
return cb(err, inst);
}
var context = {
Model: Model,
data: data,
isNewInstance: result && result.isNewInstance,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('loaded', context, function(err) {
if (err) return cb(err);
inst._initProperties(data, {persisted: true});
var context = {
Model: Model,
instance: inst,
isNewInstance: result && result.isNewInstance,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('after save', context, function(err) {
if (err) return cb(err, inst);
updateDone.call(inst, function() {
saveDone.call(inst, function() {
cb(err, inst);
});
});
});
});
}
context = {
Model: Model,
data: data,
where: byIdQuery(Model, getIdValue(Model, inst)).where,
currentInstance: inst,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return cb(err);
if (connector.save.length === 4) {
connector.save(modelName, inst.constructor._forDB(data), options, saveCallback);
} else {
connector.save(modelName, inst.constructor._forDB(data), saveCallback);
}
});
}, data, cb);
}, data, cb);
}
});
return cb.promise;
};
/**
* Update multiple instances that match the where clause
*
* Example:
*
*```js
* Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) {
* ...
* });
* ```
*
* @param {Object} [where] Search conditions (optional)
* @param {Object} data Changes to be made
* @param {Object} [options] Options for update
* @param {Function} cb Callback, called with (err, info)
*/
DataAccessObject.update =
DataAccessObject.updateAll = function(where, data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
assert(arguments.length >= 1, 'At least one argument is required');
if (data === undefined && options === undefined && cb === undefined && arguments.length === 1) {
data = where;
where = {};
} else if (options === undefined && cb === undefined) {
// One of:
// updateAll(data, cb)
// updateAll(where, data) -> Promise
if (typeof data === 'function') {
cb = data;
data = where;
where = {};
}
} else if (cb === undefined) {
// One of:
// updateAll(where, data, options) -> Promise
// updateAll(where, data, cb)
if (typeof options === 'function') {
cb = options;
options = {};
}
}
data = data || {};
options = options || {};
cb = cb || utils.createPromiseCallback();
assert(typeof where === 'object', 'The where argument must be an object');
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var Model = this;
var connector = Model.getDataSource().connector;
assert(typeof connector.update === 'function',
'update() must be implemented by the connector');
var hookState = {};
var query = {where: where};
this.applyScope(query);
this.applyProperties(data);
where = query.where;
var context = {
Model: Model,
query: {where: where},
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
var context = {
Model: Model,
where: ctx.query.where,
data: data,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context,
function(err, ctx) {
if (err) return cb(err);
doUpdate(ctx.where, ctx.data);
});
});
function doUpdate(where, data) {
try {
where = removeUndefined(where);
where = Model._coerce(where, options);
data = removeUndefined(data);
data = Model._coerce(data, options);
} catch (err) {
return process.nextTick(function() {
cb(err);
});
}
function updateCallback(err, info) {
if (err) return cb(err);
var context = {
Model: Model,
where: where,
data: data,
hookState: hookState,
options: options,
info: info,
};
Model.notifyObserversOf('after save', context, function(err, ctx) {
return cb(err, info);
});
}
var context = {
Model: Model,
where: where,
data: data,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('persist', context, function(err, ctx) {
if (err) return cb(err);
if (connector.update.length === 5) {
connector.update(Model.modelName, where, data, options, updateCallback);
} else {
connector.update(Model.modelName, where, data, updateCallback);
}
});
}
return cb.promise;
};
DataAccessObject.prototype.isNewRecord = function() {
return !this.__persisted;
};
/**
* Return connector of current record
* @private
*/
DataAccessObject.prototype.getConnector = function() {
return this.getDataSource().connector;
};
/**
* Delete object from persistence
*
* Triggers `destroy` hook (async) before and after destroying object
*
* @param {Object} [options] Options for delete
* @param {Function} cb Callback
*/
DataAccessObject.prototype.remove =
DataAccessObject.prototype.delete =
DataAccessObject.prototype.destroy = function(options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
options = options || {};
assert(typeof options === 'object', 'The options argument should be an object');
assert(typeof cb === 'function', 'The cb argument should be a function');
var inst = this;
var connector = this.getConnector();
var Model = this.constructor;
var id = getIdValue(this.constructor, this);
var hookState = {};
if (isPKMissing(Model, cb))
return cb.promise;
var context = {
Model: Model,
query: byIdQuery(Model, id),
hookState: hookState,
options: options,
};
Model.notifyObserversOf('access', context, function(err, ctx) {
if (err) return cb(err);
var context = {
Model: Model,
where: ctx.query.where,
instance: inst,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before delete', context, function(err, ctx) {
if (err) return cb(err);
doDeleteInstance(ctx.where);
});
});
function doDeleteInstance(where) {
if (!isWhereByGivenId(Model, where, id)) {
// A hook modified the query, it is no longer
// a simple 'delete model with the given id'.
// We must switch to full query-based delete.
Model.deleteAll(where, {notify: false}, function(err, info) {
if (err) return cb(err, false);
var deleted = info && info.count > 0;
if (Model.settings.strictDelete && !deleted) {
err = new Error(g.f('No instance with {{id}} %s found for %s', id, Model.modelName));
err.code = 'NOT_FOUND';
err.statusCode = 404;
return cb(err, false);
}
var context = {
Model: Model,
where: where,
instance: inst,
hookState: hookState,
options: options,
info: info,
};
Model.notifyObserversOf('after delete', context, function(err) {
cb(err, info);
});
});
return;
}
inst.trigger('destroy', function(destroyed) {
function destroyCallback(err, info) {
if (err) return cb(err);
var deleted = info && info.count > 0;
if (Model.settings.strictDelete && !deleted) {
err = new Error(g.f('No instance with {{id}} %s found for %s', id, Model.modelName));
err.code = 'NOT_FOUND';
err.statusCode = 404;
return cb(err);
}
destroyed(function() {
var context = {
Model: Model,
where: where,
instance: inst,
hookState: hookState,
options: options,
info: info,
};
Model.notifyObserversOf('after delete', context, function(err) {
cb(err, info);
});
});
}
if (connector.destroy.length === 4) {
connector.destroy(inst.constructor.modelName, id, options, destroyCallback);
} else {
connector.destroy(inst.constructor.modelName, id, destroyCallback);
}
}, null, cb);
}
return cb.promise;
};
/**
* Set a single attribute.
* Equivalent to `setAttributes({name: value})`
*
* @param {String} name Name of property
* @param {Mixed} value Value of property
*/
DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
this[name] = value; // TODO [fabien] - currently not protected by applyProperties
};
/**
* Update a single attribute.
* Equivalent to `updateAttributes({name: value}, cb)`
*
* @param {String} name Name of property
* @param {Mixed} value Value of property
* @param {Function} cb Callback function called with (err, instance)
*/
DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, options, cb) {
var data = {};
data[name] = value;
return this.updateAttributes(data, options, cb);
};
/**
* Update set of attributes.
*
* @trigger `change` hook
* @param {Object} data Data to update
*/
DataAccessObject.prototype.setAttributes = function setAttributes(data) {
if (typeof data !== 'object') return;
this.constructor.applyProperties(data, this);
var Model = this.constructor;
var inst = this;
// update instance's properties
for (var key in data) {
inst.setAttribute(key, data[key]);
}
Model.emit('set', inst);
};
DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullify) {
if (nullify || this.constructor.definition.settings.persistUndefinedAsNull) {
this[name] = this.__data[name] = null;
} else {
delete this[name];
delete this.__data[name];
}
};
/**
* Replace set of attributes.
* Performs validation before replacing.
*
* @trigger `validation`, `save` and `update` hooks
* @param {Object} data Data to replace
* @param {Object} [options] Options for replace
* @param {Function} cb Callback function called with (err, instance)
*/
DataAccessObject.prototype.replaceAttributes = function(data, options, cb) {
var Model = this.constructor;
var id = getIdValue(this.constructor, this);
return Model.replaceById(id, data, options, cb);
};
DataAccessObject.replaceById = function(id, data, options, cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (cb === undefined) {
if (typeof options === 'function') {
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
options = options || {};
assert((typeof data === 'object') && (data !== null),
'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var connector = this.getConnector();
var err;
if (typeof connector.replaceById !== 'function') {
err = new Error(g.f(
'The connector %s does not support {{replaceById}} operation. This is not a bug in LoopBack. ' +
'Please contact the authors of the connector, preferably via GitHub issues.',
connector.name));
return cb(err);
}
var pkName = idName(this);
if (!data[pkName]) data[pkName] = id;
var Model = this;
var inst = new Model(data, {persisted: true});
var enforced = {};
this.applyProperties(enforced, inst);
inst.setAttributes(enforced);
Model = this.lookupModel(data); // data-specific
if (Model !== inst.constructor) inst = new Model(data);
var strict = inst.__strict;
if (isPKMissing(Model, cb))
return cb.promise;
var model = Model.modelName;
var hookState = {};
if (id !== data[pkName]) {
err = new Error(g.f('{{id}} property (%s) ' +
'cannot be updated from %s to %s', pkName, id, data[pkName]));
err.statusCode = 400;
process.nextTick(function() { cb(err); });
return cb.promise;
}
var context = {
Model: Model,
instance: inst,
isNewInstance: false,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
if (ctx.instance[pkName] !== id && !Model._warned.cannotOverwritePKInBeforeSaveHook) {
Model._warned.cannotOverwritePKInBeforeSaveHook = true;
g.warn('WARNING: {{id}} property cannot be changed from %s to %s for model:%s ' +
'in {{\'before save\'}} operation hook', id, inst[pkName], Model.modelName);
}
data = inst.toObject(false);
if (strict) {
applyStrictCheck(Model, strict, data, inst, validateAndCallConnector);
} else {
validateAndCallConnector(null, data);
}
function validateAndCallConnector(err, data) {
if (err) return cb(err);
data = removeUndefined(data);
// update instance's properties
inst.setAttributes(data);
var doValidate = true;
if (options.validate === undefined) {
if (Model.settings.automaticValidation !== undefined) {
doValidate = Model.settings.automaticValidation;
}
} else {
doValidate = options.validate;
}
if (doValidate) {
inst.isValid(function(valid) {
if (!valid) return cb(new ValidationError(inst), inst);
callConnector();
}, data, options);
} else {
callConnector();
}
function callConnector() {
copyData(data, inst);
var typedData = convertSubsetOfPropertiesByType(inst, data);
context.data = typedData;
function replaceCallback(err, data) {
if (err) return cb(err);
var ctx = {
Model: Model,
hookState: hookState,
data: context.data,
isNewInstance: false,
options: options,
};
Model.notifyObserversOf('loaded', ctx, function(err) {
if (err) return cb(err);
if (ctx.data[pkName] !== id && !Model._warned.cannotOverwritePKInLoadedHook) {
Model._warned.cannotOverwritePKInLoadedHook = true;
g.warn('WARNING: {{id}} property cannot be changed from %s to %s for model:%s in ' +
'{{\'loaded\'}} operation hook',
id, ctx.data[pkName], Model.modelName);
}
inst.__persisted = true;
ctx.data[pkName] = id;
inst.setAttributes(ctx.data);
var context = {
Model: Model,
instance: inst,
isNewInstance: false,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, inst);
});
});
}
var ctx = {
Model: Model,
where: byIdQuery(Model, id).where,
data: context.data,
isNewInstance: false,
currentInstance: inst,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('persist', ctx, function(err) {
connector.replaceById(model, id,
inst.constructor._forDB(context.data), options, replaceCallback);
});
}
}
});
return cb.promise;
};
/**
* Update set of attributes.
* Performs validation before updating.
* NOTE: `patchOrCreate` is an alias.
*
* @trigger `validation`, `save` and `update` hooks
* @param {Object} data Data to update
* @param {Object} [options] Options for updateAttributes
* @param {Function} cb Callback function called with (err, instance)
*/
DataAccessObject.prototype.updateAttributes =
DataAccessObject.prototype.patchAttributes =
function(data, options, cb) {
var self = this;
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
if (options === undefined && cb === undefined) {
if (typeof data === 'function') {
// updateAttributes(cb)
cb = data;
data = undefined;
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// updateAttributes(data, cb)
cb = options;
options = {};
}
}
cb = cb || utils.createPromiseCallback();
options = options || {};
assert((typeof data === 'object') && (data !== null),
'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var inst = this;
var Model = this.constructor;
var connector = inst.getConnector();
assert(typeof connector.updateAttributes === 'function',
'updateAttributes() must be implemented by the connector');
if (isPKMissing(Model, cb))
return cb.promise;
var allowExtendedOperators = Model._allowExtendedOperators(options);
var strict = this.__strict;
var model = Model.modelName;
var hookState = {};
// Convert the data to be plain object so that update won't be confused
if (data instanceof Model) {
data = data.toObject(false);
}
data = removeUndefined(data);
// Make sure id(s) cannot be changed
var idNames = Model.definition.idNames();
for (var i = 0, n = idNames.length; i < n; i++) {
var idName = idNames[i];
if (data[idName] !== undefined && !idEquals(data[idName], inst[idName])) {
var err = new Error(g.f('{{id}} cannot be updated from ' +
'%s to %s when {{forceId}} is set to true',
inst[idName], data[idName]));
err.statusCode = 400;
process.nextTick(function() {
cb(err);
});
return cb.promise;
}
}
var context = {
Model: Model,
where: byIdQuery(Model, getIdValue(Model, inst)).where,
data: data,
currentInstance: inst,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
data = ctx.data;
if (strict && !allowExtendedOperators) {
applyStrictCheck(self.constructor, strict, data, inst, validateAndSave);
} else {
validateAndSave(null, data);
}
function validateAndSave(err, data) {
if (err) return cb(err);
data = removeUndefined(data);
var doValidate = true;
if (options.validate === undefined) {
if (Model.settings.automaticValidation !== undefined) {
doValidate = Model.settings.automaticValidation;
}
} else {
doValidate = options.validate;
}
// update instance's properties
try {
inst.setAttributes(data);
} catch (err) {
return cb(err);
}
if (doValidate) {
inst.isValid(function(valid) {
if (!valid) {
cb(new ValidationError(inst), inst);
return;
}
triggerSave();
}, data, options);
} else {
triggerSave();
}
function triggerSave() {
inst.trigger('save', function(saveDone) {
inst.trigger('update', function(done) {
copyData(data, inst);
var typedData = convertSubsetOfPropertiesByType(inst, data);
context.data = typedData;
function updateAttributesCallback(err) {
if (err) return cb(err);
var ctx = {
Model: Model,
data: context.data,
hookState: hookState,
options: options,
isNewInstance: false,
};
Model.notifyObserversOf('loaded', ctx, function(err) {
if (err) return cb(err);
inst.__persisted = true;
// By default, the instance passed to updateAttributes callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
if (Model.settings.updateOnLoad) {
inst.setAttributes(ctx.data);
}
done.call(inst, function() {
saveDone.call(inst, function() {
if (err) return cb(err, inst);
var context = {
Model: Model,
instance: inst,
isNewInstance: false,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, inst);
});
});
});
});
}
var ctx = {
Model: Model,
where: byIdQuery(Model, getIdValue(Model, inst)).where,
data: context.data,
currentInstance: inst,
isNewInstance: false,
hookState: hookState,
options: options,
};
Model.notifyObserversOf('persist', ctx, function(err) {
if (connector.updateAttributes.length === 5) {
connector.updateAttributes(model, getIdValue(inst.constructor, inst),
inst.constructor._forDB(context.data), options, updateAttributesCallback);
} else {
connector.updateAttributes(model, getIdValue(inst.constructor, inst),
inst.constructor._forDB(context.data), updateAttributesCallback);
}
});
}, data, cb);
}, data, cb);
}
}
});
return cb.promise;
};
/**
* Reload object from persistence
* Requires `id` member of `object` to be able to call `find`
* @param {Function} cb Called with (err, instance) arguments
* @private
*/
DataAccessObject.prototype.reload = function reload(cb) {
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
if (connectionPromise) {
return connectionPromise;
}
return this.constructor.findById(getIdValue(this.constructor, this), cb);
};
/*
* Define readonly property on object
*
* @param {Object} obj
* @param {String} key
* @param {Mixed} value
* @private
*/
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
enumerable: true,
configurable: true,
value: value,
});
}
var defineScope = require('./scope.js').defineScope;
/**
* Define a scope for the model class. Scopes enable you to specify commonly-used
* queries that you can reference as method calls on a model.
*
* @param {String} name The scope name
* @param {Object} query The query object for DataAccessObject.find()
* @param {ModelClass} [targetClass] The model class for the query, default to
* the declaring model
*/
DataAccessObject.scope = function(name, query, targetClass, methods, options) {
var cls = this;
if (options && options.isStatic === false) {
cls = cls.prototype;
}
return defineScope(cls, targetClass || cls, name, query, methods, options);
};
/*
* Add 'include'
*/
jutil.mixin(DataAccessObject, Inclusion);
/*
* Add 'relation'
*/
jutil.mixin(DataAccessObject, Relation);
/*
* Add 'transaction'
*/
jutil.mixin(DataAccessObject, require('./transaction'));
function PKMissingError(modelName) {
this.name = 'PKMissingError';
this.message = 'Primary key is missing for the ' + modelName + ' model';
}
PKMissingError.prototype = new Error();
function isPKMissing(modelClass, cb) {
var hasPK = modelClass.definition.hasPK();
if (hasPK) return false;
process.nextTick(function() {
cb(new PKMissingError(modelClass.modelName));
});
return true;
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// Turning on strict for this file breaks lots of test cases;
// disabling strict for this file
/* eslint-disable strict */
/*!
* Module dependencies
*/
var ModelBuilder = require('./model-builder.js').ModelBuilder;
var ModelDefinition = require('./model-definition.js');
var RelationDefinition = require('./relation-definition.js');
var OberserverMixin = require('./observer');
var jutil = require('./jutil');
var utils = require('./utils');
var ModelBaseClass = require('./model.js');
var DataAccessObject = require('./dao.js');
var defineScope = require('./scope.js').defineScope;
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var assert = require('assert');
var async = require('async');
var traverse = require('traverse');
var g = require('strong-globalize')();
var juggler = require('..');
Iif (process.env.DEBUG === 'loopback') {
// For back-compatibility
process.env.DEBUG = 'loopback:*';
}
var debug = require('debug')('loopback:datasource');
/*!
* Export public API
*/
exports.DataSource = DataSource;
/*!
* Helpers
*/
var slice = Array.prototype.slice;
/**
* LoopBack models can manipulate data via the DataSource object.
* Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`.
*
* Define a data source to persist model data.
* To create a DataSource programmatically, call `createDataSource()` on the LoopBack object; for example:
* ```js
* var oracle = loopback.createDataSource({
* connector: 'oracle',
* host: '111.22.333.44',
* database: 'MYDB',
* username: 'username',
* password: 'password'
* });
* ```
*
* All classes in single dataSource share same the connector type and
* one database connection.
*
* For example, the following creates a DataSource, and waits for a connection callback.
*
* ```
* var dataSource = new DataSource('mysql', { database: 'myapp_test' });
* dataSource.define(...);
* dataSource.on('connected', function () {
* // work with database
* });
* ```
* @class DataSource
* @param {String} [name] Optional name for datasource.
* @options {Object} settings Database-specific settings to establish connection (settings depend on specific connector).
* The table below lists a typical set for a relational database.
* @property {String} connector Database connector to use. For any supported connector, can be any of:
*
* - The connector module from `require(connectorName)`.
* - The full name of the connector module, such as 'loopback-connector-oracle'.
* - The short name of the connector module, such as 'oracle'.
* - A local module under `./connectors/` folder.
* @property {String} host Database server host name.
* @property {String} port Database server port number.
* @property {String} username Database user name.
* @property {String} password Database password.
* @property {String} database Name of the database to use.
* @property {Boolean} debug Display debugging information. Default is false.
*/
function DataSource(name, settings, modelBuilder) {
if (!(this instanceof DataSource)) {
return new DataSource(name, settings);
}
// Check if the settings object is passed as the first argument
if (typeof name === 'object' && settings === undefined) {
settings = name;
name = undefined;
}
// Check if the first argument is a URL
if (typeof name === 'string' && name.indexOf('://') !== -1) {
name = utils.parseSettings(name);
}
// Check if the settings is in the form of URL string
if (typeof settings === 'string' && settings.indexOf('://') !== -1) {
settings = utils.parseSettings(settings);
}
this.modelBuilder = modelBuilder || new ModelBuilder();
this.models = this.modelBuilder.models;
this.definitions = this.modelBuilder.definitions;
this.juggler = juggler;
// operation metadata
// Initialize it before calling setup as the connector might register operations
this._operations = {};
this.setup(name, settings);
this._setupConnector();
// connector
var connector = this.connector;
// DataAccessObject - connector defined or supply the default
var dao = (connector && connector.DataAccessObject) || this.constructor.DataAccessObject;
this.DataAccessObject = function() {
};
// define DataAccessObject methods
Object.keys(dao).forEach(function(name) {
var fn = dao[name];
this.DataAccessObject[name] = fn;
if (typeof fn === 'function') {
this.defineOperation(name, {
accepts: fn.accepts,
'returns': fn.returns,
http: fn.http,
remoteEnabled: fn.shared ? true : false,
scope: this.DataAccessObject,
fnName: name,
});
}
}.bind(this));
// define DataAccessObject.prototype methods
Object.keys(dao.prototype).forEach(function(name) {
var fn = dao.prototype[name];
this.DataAccessObject.prototype[name] = fn;
if (typeof fn === 'function') {
this.defineOperation(name, {
prototype: true,
accepts: fn.accepts,
'returns': fn.returns,
http: fn.http,
remoteEnabled: fn.shared ? true : false,
scope: this.DataAccessObject.prototype,
fnName: name,
});
}
}.bind(this));
}
util.inherits(DataSource, EventEmitter);
// allow child classes to supply a data access object
DataSource.DataAccessObject = DataAccessObject;
/**
* Set up the connector instance for backward compatibility with JugglingDB schema/adapter
* @private
*/
DataSource.prototype._setupConnector = function() {
this.connector = this.connector || this.adapter; // The legacy JugglingDB adapter will set up `adapter` property
this.adapter = this.connector; // Keep the adapter as an alias to connector
if (this.connector) {
if (!this.connector.dataSource) {
// Set up the dataSource if the connector doesn't do so
this.connector.dataSource = this;
}
var dataSource = this;
this.connector.log = function(query, start) {
dataSource.log(query, start);
};
this.connector.logger = function(query) {
var t1 = Date.now();
var log = this.log;
return function(q) {
log(q || query, t1);
};
};
// Configure the connector instance to mix in observer functions
jutil.mixin(this.connector, OberserverMixin);
}
};
// List possible connector module names
function connectorModuleNames(name) {
var names = []; // Check the name as is
if (!name.match(/^\//)) {
names.push('./connectors/' + name); // Check built-in connectors
if (name.indexOf('loopback-connector-') !== 0) {
names.push('loopback-connector-' + name); // Try loopback-connector-<name>
}
}
// Only try the short name if the connector is not from StrongLoop
if (['mongodb', 'oracle', 'mysql', 'postgresql', 'mssql', 'rest', 'soap', 'db2', 'cloudant']
.indexOf(name) === -1) {
names.push(name);
}
return names;
}
// testable with DI
function tryModules(names, loader) {
var mod;
loader = loader || require;
for (var m = 0; m < names.length; m++) {
try {
mod = loader(names[m]);
} catch (e) {
var notFound = e.code === 'MODULE_NOT_FOUND' &&
e.message && e.message.indexOf(names[m]) > 0;
if (notFound) {
debug('Module %s not found, will try another candidate.', names[m]);
continue;
}
debug('Cannot load connector %s: %s', names[m], e.stack || e);
throw e;
}
if (mod) {
break;
}
}
return mod;
}
/*!
* Resolve a connector by name
* @param name The connector name
* @returns {*}
* @private
*/
DataSource._resolveConnector = function(name, loader) {
var names = connectorModuleNames(name);
var connector = tryModules(names, loader);
var error = null;
if (!connector) {
error = g.f('\nWARNING: {{LoopBack}} connector "%s" is not installed ' +
'as any of the following modules:\n\n %s\n\nTo fix, run:\n\n {{npm install %s --save}}\n',
name, names.join('\n'), names[names.length - 1]);
}
return {
connector: connector,
error: error,
};
};
/**
* Connect to the data source
* @param callback
*/
DataSource.prototype.connect = function(callback) {
callback = callback || utils.createPromiseCallback();
var self = this;
if (this.connected) {
// The data source is already connected, return immediately
process.nextTick(callback);
return callback.promise;
}
if (typeof this.connector.connect !== 'function') {
// Connector doesn't have the connect function
// Assume no connect is needed
self.connected = true;
self.connecting = false;
process.nextTick(function() {
self.emit('connected');
callback();
});
return callback.promise;
}
// Queue the callback
this.pendingConnectCallbacks = this.pendingConnectCallbacks || [];
this.pendingConnectCallbacks.push(callback);
// The connect is already in progress
if (this.connecting) return callback.promise;
// Set connecting flag to be true
this.connecting = true;
this.connector.connect(function(err, result) {
self.connecting = false;
if (!err) self.connected = true;
var cbs = self.pendingConnectCallbacks;
self.pendingConnectCallbacks = [];
if (!err) {
self.emit('connected');
} else {
self.emit('error', err);
}
// Invoke all pending callbacks
async.each(cbs, function(cb, done) {
try {
cb(err);
} catch (e) {
// Ignore error to make sure all callbacks are invoked
debug('Uncaught error raised by connect callback function: ', e);
} finally {
done();
}
}, function(err) {
if (err) throw err; // It should not happen
});
});
return callback.promise;
};
/**
* Set up the data source
* @param {String} name The name
* @param {Object} settings The settings
* @returns {*}
* @private
*/
DataSource.prototype.setup = function(name, settings) {
var dataSource = this;
var connector;
// support single settings object
if (name && typeof name === 'object' && !settings) {
settings = name;
name = undefined;
}
if (typeof settings === 'object') {
if (settings.initialize) {
connector = settings;
} else if (settings.connector) {
connector = settings.connector;
} else if (settings.adapter) {
connector = settings.adapter;
}
}
// just save everything we get
this.settings = settings || {};
this.settings.debug = this.settings.debug || debug.enabled;
if (this.settings.debug) {
debug('Settings: %j', this.settings);
}
// Disconnected by default
this.connected = false;
this.connecting = false;
if (typeof connector === 'string') {
name = connector;
connector = undefined;
}
name = name || (connector && connector.name);
this.name = name;
if (name && !connector) {
if (typeof name === 'object') {
// The first argument might be the connector itself
connector = name;
this.name = connector.name;
} else {
// The connector has not been resolved
var result = DataSource._resolveConnector(name);
connector = result.connector;
if (!connector) {
console.error(result.error);
this.emit('error', new Error(result.error));
return;
}
}
}
if (connector) {
var postInit = function postInit(err, result) {
this._setupConnector();
// we have an connector now?
if (!this.connector) {
throw new Error(g.f('Connector is not defined correctly: ' +
'it should create `{{connector}}` member of dataSource'));
}
this.connected = !err; // Connected now
if (this.connected) {
this.emit('connected');
} else {
// The connection fails, let's report it and hope it will be recovered in the next call
g.error('Connection fails: %s\nIt will be retried for the next request.', err);
this.emit('error', err);
this.connecting = false;
}
}.bind(this);
try {
if ('function' === typeof connector.initialize) {
// Call the async initialize method
connector.initialize(this, postInit);
} else if ('function' === typeof connector) {
// Use the connector constructor directly
this.connector = new connector(this.settings);
postInit();
}
} catch (err) {
if (err.message) {
err.message = 'Cannot initialize connector ' +
JSON.stringify(connector.name || name) + ': ' +
err.message;
}
throw err;
}
}
};
function isModelClass(cls) {
if (!cls) {
return false;
}
return cls.prototype instanceof ModelBaseClass;
}
DataSource.relationTypes = Object.keys(RelationDefinition.RelationTypes);
function isModelDataSourceAttached(model) {
return model && (!model.settings.unresolved) && (model.dataSource instanceof DataSource);
}
/*!
* Define scopes for the model class from the scopes object
* @param modelClass
* @param scopes
*/
DataSource.prototype.defineScopes = function(modelClass, scopes) {
if (scopes) {
for (var s in scopes) {
defineScope(modelClass, modelClass, s, scopes[s], {}, scopes[s].options);
}
}
};
/*!
* Define relations for the model class from the relations object
* @param modelClass
* @param relations
*/
DataSource.prototype.defineRelations = function(modelClass, relations) {
var self = this;
// Create a function for the closure in the loop
var createListener = function(name, relation, targetModel, throughModel) {
if (!isModelDataSourceAttached(targetModel)) {
targetModel.once('dataAccessConfigured', function(model) {
// Check if the through model doesn't exist or resolved
if (!throughModel || isModelDataSourceAttached(throughModel)) {
// The target model is resolved
var params = traverse(relation).clone();
params.as = name;
params.model = model;
if (throughModel) {
params.through = throughModel;
}
modelClass[relation.type].call(modelClass, name, params);
}
});
}
if (throughModel && !isModelDataSourceAttached(throughModel)) {
// Set up a listener to the through model
throughModel.once('dataAccessConfigured', function(model) {
if (isModelDataSourceAttached(targetModel)) {
// The target model is resolved
var params = traverse(relation).clone();
params.as = name;
params.model = targetModel;
params.through = model;
modelClass[relation.type].call(modelClass, name, params);
}
});
}
};
// Set up the relations
if (relations) {
Object.keys(relations).forEach(function(rn) {
var r = relations[rn];
assert(DataSource.relationTypes.indexOf(r.type) !== -1, 'Invalid relation type: ' + r.type);
assert(isValidRelationName(rn), 'Invalid relation name: ' + rn);
var targetModel, polymorphicName;
if (r.polymorphic && r.type !== 'belongsTo' && !r.model) {
throw new Error(g.f('No model specified for {{polymorphic}} %s: %s', r.type, rn));
}
if (r.polymorphic) {
polymorphicName = typeof r.model === 'string' ? r.model : rn;
if (typeof r.polymorphic === 'string') {
polymorphicName = r.polymorphic;
} else if (typeof r.polymorphic === 'object' && typeof r.polymorphic.as === 'string') {
polymorphicName = r.polymorphic.as;
}
}
if (r.model) {
targetModel = isModelClass(r.model) ? r.model : self.getModel(r.model, true);
}
var throughModel = null;
if (r.through) {
throughModel = isModelClass(r.through) ? r.through : self.getModel(r.through, true);
}
if ((targetModel && !isModelDataSourceAttached(targetModel)) ||
(throughModel && !isModelDataSourceAttached(throughModel))) {
// Create a listener to defer the relation set up
createListener(rn, r, targetModel, throughModel);
} else {
// The target model is resolved
var params = traverse(r).clone();
params.as = rn;
params.model = polymorphicName || targetModel;
if (throughModel) {
params.through = throughModel;
}
modelClass[r.type].call(modelClass, rn, params);
}
});
}
};
function isValidRelationName(relationName) {
var invalidRelationNames = ['trigger'];
return invalidRelationNames.indexOf(relationName) === -1;
}
/*!
* Set up the data access functions from the data source
* @param {Model} modelClass The model class
* @param {Object} settings The settings object
*/
DataSource.prototype.setupDataAccess = function(modelClass, settings) {
if (this.connector) {
// Check if the id property should be generated
var idName = modelClass.definition.idName();
var idProp = modelClass.definition.rawProperties[idName];
if (idProp && idProp.generated && this.connector.getDefaultIdType) {
// Set the default id type from connector's ability
var idType = this.connector.getDefaultIdType() || String;
idProp.type = idType;
modelClass.definition.rawProperties[idName].type = idType;
modelClass.definition.properties[idName].type = idType;
var forceId = settings.forceId;
if (idProp.generated && forceId !== false) {
forceId = true;
}
if (forceId) {
modelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});
}
}
if (this.connector.define) {
// pass control to connector
this.connector.define({
model: modelClass,
properties: modelClass.definition.properties,
settings: settings,
});
}
}
// add data access objects
this.mixin(modelClass);
// define relations from LDL (options.relations)
var relations = settings.relationships || settings.relations;
this.defineRelations(modelClass, relations);
// Emit the dataAccessConfigured event to indicate all the methods for data
// access have been mixed into the model class
modelClass.emit('dataAccessConfigured', modelClass);
// define scopes from LDL (options.relations)
var scopes = settings.scopes || {};
this.defineScopes(modelClass, scopes);
};
/**
* Define a model class. Returns newly created model object.
* The first (String) argument specifying the model name is required.
* You can provide one or two JSON object arguments, to provide configuration options.
* See [Model definition reference](http://docs.strongloop.com/display/DOC/Model+definition+reference) for details.
*
* Simple example:
* ```
* var User = dataSource.createModel('User', {
* email: String,
* password: String,
* birthDate: Date,
* activated: Boolean
* });
* ```
* More advanced example
* ```
* var User = dataSource.createModel('User', {
* email: { type: String, limit: 150, index: true },
* password: { type: String, limit: 50 },
* birthDate: Date,
* registrationDate: {type: Date, default: function () { return new Date }},
* activated: { type: Boolean, default: false }
* });
* ```
* You can also define an ACL when you create a new data source with the `DataSource.create()` method. For example:
*
* ```js
* var Customer = ds.createModel('Customer', {
* name: {
* type: String,
* acls: [
* {principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
* {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
* ]
* }
* }, {
* acls: [
* {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
* ]
* });
* ```
*
* @param {String} className Name of the model to create.
* @param {Object} properties Hash of model properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
* @options {Object} properties Other configuration options. This corresponds to the options key in the config object.
*
*/
DataSource.prototype.createModel =
DataSource.prototype.define = function defineClass(className, properties, settings) {
var args = slice.call(arguments);
if (!className) {
throw new Error(g.f('Class name required'));
}
if (args.length === 1) {
properties = {};
args.push(properties);
}
if (args.length === 2) {
settings = {};
args.push(settings);
}
properties = properties || {};
settings = settings || {};
if (this.isRelational()) {
// Set the strict mode to be true for relational DBs by default
if (settings.strict === undefined || settings.strict === null) {
settings.strict = true;
}
if (settings.strict === false) {
settings.strict = 'throw';
}
}
var modelClass = this.modelBuilder.define(className, properties, settings);
modelClass.dataSource = this;
if (settings.unresolved) {
return modelClass;
}
this.setupDataAccess(modelClass, settings);
modelClass.emit('dataSourceAttached', modelClass);
return modelClass;
};
/**
* Mixin DataAccessObject methods.
*
* @param {Function} ModelCtor The model constructor
* @private
*/
DataSource.prototype.mixin = function(ModelCtor) {
var ops = this.operations();
var DAO = this.DataAccessObject;
// mixin DAO
jutil.mixin(ModelCtor, DAO, {proxyFunctions: true, override: true});
// decorate operations as alias functions
Object.keys(ops).forEach(function(name) {
var op = ops[name];
var scope;
if (op.enabled) {
scope = op.prototype ? ModelCtor.prototype : ModelCtor;
// var sfn = scope[name] = function () {
// op.scope[op.fnName].apply(self, arguments);
// }
Object.keys(op)
.filter(function(key) {
// filter out the following keys
return ~[
'scope',
'fnName',
'prototype',
].indexOf(key);
})
.forEach(function(key) {
if (typeof op[key] !== 'undefined') {
op.scope[op.fnName][key] = op[key];
}
});
}
});
};
/**
* See ModelBuilder.getModel
*/
DataSource.prototype.getModel = function(name, forceCreate) {
return this.modelBuilder.getModel(name, forceCreate);
};
/**
* See ModelBuilder.getModelDefinition
*/
DataSource.prototype.getModelDefinition = function(name) {
return this.modelBuilder.getModelDefinition(name);
};
/**
* Get the data source types
* @returns {String[]} The data source type, such as ['db', 'nosql', 'mongodb'],
* ['rest'], or ['db', 'rdbms', 'mysql']
*/
DataSource.prototype.getTypes = function() {
var getTypes = this.connector && this.connector.getTypes;
var types = getTypes && getTypes() || [];
if (typeof types === 'string') {
types = types.split(/[\s,\/]+/);
}
return types;
};
/**
* Check the data source supports the specified types.
* @param {String} types Type name or an array of type names. Can also be array of Strings.
* @returns {Boolean} true if all types are supported by the data source
*/
DataSource.prototype.supportTypes = function(types) {
var supportedTypes = this.getTypes();
if (Array.isArray(types)) {
// Check each of the types
for (var i = 0; i < types.length; i++) {
if (supportedTypes.indexOf(types[i]) === -1) {
// Not supported
return false;
}
}
return true;
} else {
// The types is a string
return supportedTypes.indexOf(types) !== -1;
}
};
/**
* Attach an existing model to a data source.
*
* @param {Function} modelClass The model constructor
*/
DataSource.prototype.attach = function(modelClass) {
if (modelClass.dataSource === this) {
// Already attached to the data source
return modelClass;
}
if (modelClass.modelBuilder !== this.modelBuilder) {
this.modelBuilder.definitions[modelClass.modelName] = modelClass.definition;
this.modelBuilder.models[modelClass.modelName] = modelClass;
// reset the modelBuilder
modelClass.modelBuilder = this.modelBuilder;
}
// redefine the dataSource
modelClass.dataSource = this;
this.setupDataAccess(modelClass, modelClass.settings);
modelClass.emit('dataSourceAttached', modelClass);
return modelClass;
};
/**
* Define single property named `prop` on `model`
*
* @param {String} model Name of model
* @param {String} prop Name of property
* @param {Object} params Property settings
*/
DataSource.prototype.defineProperty = function(model, prop, params) {
this.modelBuilder.defineProperty(model, prop, params);
var resolvedProp = this.getModelDefinition(model).properties[prop];
if (this.connector && this.connector.defineProperty) {
this.connector.defineProperty(model, prop, resolvedProp);
}
};
/**
* Drop each model table and re-create.
* This method applies only to database connectors. For MongoDB, it drops and creates indexes.
*
* **WARNING**: Calling this function deletes all data! Use `autoupdate()` to preserve data.
*
* @param {String} model Model to migrate. If not present, apply to all models. Can also be an array of Strings.
* @param {Function} [callback] Callback function. Optional.
*
*/
DataSource.prototype.automigrate = function(models, cb) {
this.freeze();
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
cb = cb || utils.createPromiseCallback();
if (!this.connector.automigrate) {
// NOOP
process.nextTick(cb);
return cb.promise;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
var attachedModels = this.connector._models;
if (attachedModels && typeof attachedModels === 'object') {
models = models || Object.keys(attachedModels);
if (models.length === 0) {
process.nextTick(cb);
return cb.promise;
}
var invalidModels = models.filter(function(m) {
return !(m in attachedModels);
});
if (invalidModels.length) {
process.nextTick(function() {
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
return cb.promise;
}
}
this.connector.automigrate(models, cb);
return cb.promise;
};
/**
* Update existing database tables.
* This method applies only to database connectors.
*
* @param {String} model Model to migrate. If not present, apply to all models. Can also be an array of Strings.
* @param {Function} [cb] The callback function
*/
DataSource.prototype.autoupdate = function(models, cb) {
this.freeze();
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
cb = cb || utils.createPromiseCallback();
if (!this.connector.autoupdate) {
// NOOP
process.nextTick(cb);
return cb.promise;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
var attachedModels = this.connector._models;
if (attachedModels && typeof attachedModels === 'object') {
models = models || Object.keys(attachedModels);
if (models.length === 0) {
process.nextTick(cb);
return cb.promise;
}
var invalidModels = models.filter(function(m) {
return !(m in attachedModels);
});
if (invalidModels.length) {
process.nextTick(function() {
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
return cb.promise;
}
}
this.connector.autoupdate(models, cb);
return cb.promise;
};
/**
* Discover existing database tables.
* This method returns an array of model objects, including {type, name, onwer}
*
* @param {Object} options The options
* @param {Function} Callback function. Optional.
* @options {Object} options Discovery options. See below.
* @property {String} owner/schema The owner or schema to discover from.
* @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
* @property {Boolean} views If true, include views; if false, only tables.
* @property {Number} limit Page size
* @property {Number} offset Starting index
*
*/
DataSource.prototype.discoverModelDefinitions = function(options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverModelDefinitions) {
this.connector.discoverModelDefinitions(options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/**
* The synchronous version of discoverModelDefinitions.
* @options {Object} options The options
* @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
* @property {Boolean} views If true, nclude views; if false, only tables.
* @property {Number} limit Page size
* @property {Number} offset Starting index
* @returns {*}
*/
DataSource.prototype.discoverModelDefinitionsSync = function(options) {
this.freeze();
if (this.connector.discoverModelDefinitionsSync) {
return this.connector.discoverModelDefinitionsSync(options);
}
return null;
};
/**
* Discover properties for a given model.
*
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*|owner | String | Database owner or schema|
*|tableName | String | Table/view name|
*|columnName | String | Column name|
*|dataType | String | Data type|
*|dataLength | Number | Data length|
*|dataPrecision | Number | Numeric data precision|
*|dataScale |Number | Numeric data scale|
*|nullable |Boolean | If true, then the data can be null|
*
* @param {String} modelName The table/view name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} cb Callback function. Optional
*
*/
DataSource.prototype.discoverModelProperties = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverModelProperties) {
this.connector.discoverModelProperties(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/**
* The synchronous version of discoverModelProperties
* @param {String} modelName The table/view name
* @param {Object} options The options
* @returns {*}
*/
DataSource.prototype.discoverModelPropertiesSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverModelPropertiesSync) {
return this.connector.discoverModelPropertiesSync(modelName, options);
}
return null;
};
/**
* Discover primary keys for a given owner/modelName.
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*| owner |String | Table schema or owner (may be null). Owner defaults to current user.
*| tableName |String| Table name
*| columnName |String| Column name
*| keySeq |Number| Sequence number within primary key (1 indicates the first column in the primary key; 2 indicates the second column in the primary key).
*| pkName |String| Primary key name (may be null)
*
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} [cb] The callback function
*/
DataSource.prototype.discoverPrimaryKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverPrimaryKeys) {
this.connector.discoverPrimaryKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/**
* The synchronous version of discoverPrimaryKeys
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner orschema
* @returns {*}
*/
DataSource.prototype.discoverPrimaryKeysSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverPrimaryKeysSync) {
return this.connector.discoverPrimaryKeysSync(modelName, options);
}
return null;
};
/**
* Discover foreign keys for a given owner/modelName
*
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*|fkOwner |String | Foreign key table schema (may be null)
*|fkName |String | Foreign key name (may be null)
*|fkTableName |String | Foreign key table name
*|fkColumnName |String | Foreign key column name
*|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
*|pkOwner |String | Primary key table schema being imported (may be null)
*|pkName |String | Primary key name (may be null)
*|pkTableName |String | Primary key table name being imported
*|pkColumnName |String | Primary key column name being imported
*
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} [cb] The callback function
*
*/
DataSource.prototype.discoverForeignKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverForeignKeys) {
this.connector.discoverForeignKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/**
* The synchronous version of discoverForeignKeys
*
* @param {String} modelName The model name
* @param {Object} options The options
* @returns {*}
*/
DataSource.prototype.discoverForeignKeysSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverForeignKeysSync) {
return this.connector.discoverForeignKeysSync(modelName, options);
}
return null;
};
/**
* Retrieves a description of the foreign key columns that reference the given table's primary key columns
* (the foreign keys exported by a table), ordered by fkTableOwner, fkTableName, and keySeq.
*
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*|fkOwner |String | Foreign key table schema (may be null)
*|fkName |String | Foreign key name (may be null)
*|fkTableName |String | Foreign key table name
*|fkColumnName |String | Foreign key column name
*|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
*|pkOwner |String | Primary key table schema being imported (may be null)
*|pkName |String | Primary key name (may be null)
*|pkTableName |String | Primary key table name being imported
*|pkColumnName |String | Primary key column name being imported
*
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} [cb] The callback function
*/
DataSource.prototype.discoverExportedForeignKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverExportedForeignKeys) {
this.connector.discoverExportedForeignKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/**
* The synchronous version of discoverExportedForeignKeys
* @param {String} modelName The model name
* @param {Object} options The options
* @returns {*}
*/
DataSource.prototype.discoverExportedForeignKeysSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverExportedForeignKeysSync) {
return this.connector.discoverExportedForeignKeysSync(modelName, options);
}
return null;
};
function capitalize(str) {
if (!str) {
return str;
}
return str.charAt(0).toUpperCase() + ((str.length > 1) ? str.slice(1).toLowerCase() : '');
}
function fromDBName(dbName, camelCase) {
if (!dbName) {
return dbName;
}
var parts = dbName.split(/-|_/);
parts[0] = camelCase ? parts[0].toLowerCase() : capitalize(parts[0]);
for (var i = 1; i < parts.length; i++) {
parts[i] = capitalize(parts[i]);
}
return parts.join('');
}
/**
* Discover one schema from the given model without following the relations.
**Example schema from oracle connector:**
*
* ```js
* {
* "name": "Product",
* "options": {
* "idInjection": false,
* "oracle": {
* "schema": "BLACKPOOL",
* "table": "PRODUCT"
* }
* },
* "properties": {
* "id": {
* "type": "String",
* "required": true,
* "length": 20,
* "id": 1,
* "oracle": {
* "columnName": "ID",
* "dataType": "VARCHAR2",
* "dataLength": 20,
* "nullable": "N"
* }
* },
* "name": {
* "type": "String",
* "required": false,
* "length": 64,
* "oracle": {
* "columnName": "NAME",
* "dataType": "VARCHAR2",
* "dataLength": 64,
* "nullable": "Y"
* }
* },
* ...
* "fireModes": {
* "type": "String",
* "required": false,
* "length": 64,
* "oracle": {
* "columnName": "FIRE_MODES",
* "dataType": "VARCHAR2",
* "dataLength": 64,
* "nullable": "Y"
* }
* }
* }
* }
* ```
*
* @param {String} modelName The model name
* @param {Object} [options] The options
* @param {Function} [cb] The callback function
*/
DataSource.prototype.discoverSchema = function(modelName, options, cb) {
options = options || {};
if (!cb && 'function' === typeof options) {
cb = options;
options = {};
}
options.visited = {};
options.relations = false;
cb = cb || utils.createPromiseCallback();
this.discoverSchemas(modelName, options, function(err, schemas) {
if (err || !schemas) {
cb && cb(err, schemas);
return;
}
for (var s in schemas) {
cb && cb(null, schemas[s]);
return;
}
});
return cb.promise;
};
/**
* Discover schema from a given modelName/view.
*
* @param {String} modelName The model name.
* @options {Object} [options] Options; see below.
* @property {String} owner|schema Database owner or schema name.
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
* @property {Boolean} all True if all owners are included; false otherwise.
* @property {Boolean} views True if views are included; false otherwise.
* @param {Function} [cb] The callback function
*/
DataSource.prototype.discoverSchemas = function(modelName, options, cb) {
options = options || {};
if (!cb && 'function' === typeof options) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var self = this;
var dbType = this.connector.name || this.name;
var nameMapper;
if (options.nameMapper === null) {
// No mapping
nameMapper = function(type, name) {
return name;
};
} else if (typeof options.nameMapper === 'function') {
// Custom name mapper
nameMapper = options.nameMapper;
} else {
// Default name mapper
nameMapper = function mapName(type, name) {
if (type === 'table' || type === 'model') {
return fromDBName(name, false);
} else if (type == 'fk') {
return fromDBName(name + 'Rel', true);
} else {
return fromDBName(name, true);
}
};
}
if (this.connector.discoverSchemas) {
// Delegate to the connector implementation
this.connector.discoverSchemas(modelName, options, cb);
return cb.promise;
}
var tasks = [
this.discoverModelProperties.bind(this, modelName, options),
this.discoverPrimaryKeys.bind(this, modelName, options)];
var followingRelations = options.associations || options.relations;
if (followingRelations) {
tasks.push(this.discoverForeignKeys.bind(this, modelName, options));
}
async.parallel(tasks, function(err, results) {
if (err) {
cb(err);
return cb.promise;
}
var columns = results[0];
if (!columns || columns.length === 0) {
cb(new Error(g.f('Table \'%s\' does not exist.', modelName)));
return cb.promise;
}
// Handle primary keys
var primaryKeys = results[1] || [];
var pks = {};
primaryKeys.forEach(function(pk) {
pks[pk.columnName] = pk.keySeq;
});
if (self.settings.debug) {
debug('Primary keys: ', pks);
}
var schema = {
name: nameMapper('table', modelName),
options: {
idInjection: false, // DO NOT add id property
},
properties: {},
};
schema.options[dbType] = {
schema: columns[0].owner,
table: modelName,
};
columns.forEach(function(item) {
var propName = nameMapper('column', item.columnName);
schema.properties[propName] = {
type: item.type,
required: (item.nullable === 'N' || item.nullable === 'NO' ||
item.nullable === 0 || item.nullable === false),
length: item.dataLength,
precision: item.dataPrecision,
scale: item.dataScale,
};
if (pks[item.columnName]) {
schema.properties[propName].id = pks[item.columnName];
}
var dbSpecific = schema.properties[propName][dbType] = {
columnName: item.columnName,
dataType: item.dataType,
dataLength: item.dataLength,
dataPrecision: item.dataPrecision,
dataScale: item.dataScale,
nullable: item.nullable,
};
// merge connector-specific properties
if (item[dbType]) {
for (var k in item[dbType]) {
dbSpecific[k] = item[dbType][k];
}
}
});
// Add current modelName to the visited tables
options.visited = options.visited || {};
var schemaKey = columns[0].owner + '.' + modelName;
if (!options.visited.hasOwnProperty(schemaKey)) {
if (self.settings.debug) {
debug('Adding schema for ' + schemaKey);
}
options.visited[schemaKey] = schema;
}
var otherTables = {};
if (followingRelations) {
// Handle foreign keys
var fks = {};
var foreignKeys = results[2] || [];
foreignKeys.forEach(function(fk) {
var fkInfo = {
keySeq: fk.keySeq,
owner: fk.pkOwner,
tableName: fk.pkTableName,
columnName: fk.pkColumnName,
};
if (fks[fk.fkName]) {
fks[fk.fkName].push(fkInfo);
} else {
fks[fk.fkName] = [fkInfo];
}
});
if (self.settings.debug) {
debug('Foreign keys: ', fks);
}
schema.options.relations = {};
foreignKeys.forEach(function(fk) {
var propName = nameMapper('fk', (fk.fkName || fk.pkTableName));
schema.options.relations[propName] = {
model: nameMapper('table', fk.pkTableName),
type: 'belongsTo',
foreignKey: nameMapper('column', fk.fkColumnName),
};
var key = fk.pkOwner + '.' + fk.pkTableName;
if (!options.visited.hasOwnProperty(key) && !otherTables.hasOwnProperty(key)) {
otherTables[key] = {owner: fk.pkOwner, tableName: fk.pkTableName};
}
});
}
if (Object.keys(otherTables).length === 0) {
cb(null, options.visited);
} else {
var moreTasks = [];
for (var t in otherTables) {
if (self.settings.debug) {
debug('Discovering related schema for ' + schemaKey);
}
var newOptions = {};
for (var key in options) {
newOptions[key] = options[key];
}
newOptions.owner = otherTables[t].owner;
moreTasks.push(DataSource.prototype.discoverSchemas.bind(self, otherTables[t].tableName, newOptions));
}
async.parallel(moreTasks, function(err, results) {
var result = results && results[0];
cb(err, result);
});
}
});
return cb.promise;
};
/**
* Discover schema from a given table/view synchronously
*
* @param {String} modelName The model name
* @options {Object} [options] Options; see below.
* @property {String} owner|schema Database owner or schema name.
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
* @property {Boolean} all True if all owners are included; false otherwise.
* @property {Boolean} views True if views are included; false otherwise.
*/
DataSource.prototype.discoverSchemasSync = function(modelName, options) {
var self = this;
var dbType = this.name || this.connector.name;
var columns = this.discoverModelPropertiesSync(modelName, options);
if (!columns || columns.length === 0) {
return [];
}
var nameMapper = options.nameMapper || function mapName(type, name) {
if (type === 'table' || type === 'model') {
return fromDBName(name, false);
} else {
return fromDBName(name, true);
}
};
// Handle primary keys
var primaryKeys = this.discoverPrimaryKeysSync(modelName, options);
var pks = {};
primaryKeys.forEach(function(pk) {
pks[pk.columnName] = pk.keySeq;
});
if (self.settings.debug) {
debug('Primary keys: ', pks);
}
var schema = {
name: nameMapper('table', modelName),
options: {
idInjection: false, // DO NOT add id property
},
properties: {},
};
schema.options[dbType] = {
schema: columns.length > 0 && columns[0].owner,
table: modelName,
};
columns.forEach(function(item) {
var i = item;
var propName = nameMapper('column', item.columnName);
schema.properties[propName] = {
type: item.type,
required: (item.nullable === 'N'),
length: item.dataLength,
precision: item.dataPrecision,
scale: item.dataScale,
};
if (pks[item.columnName]) {
schema.properties[propName].id = pks[item.columnName];
}
schema.properties[propName][dbType] = {
columnName: i.columnName,
dataType: i.dataType,
dataLength: i.dataLength,
dataPrecision: item.dataPrecision,
dataScale: item.dataScale,
nullable: i.nullable,
};
});
// Add current modelName to the visited tables
options.visited = options.visited || {};
var schemaKey = columns[0].owner + '.' + modelName;
if (!options.visited.hasOwnProperty(schemaKey)) {
if (self.settings.debug) {
debug('Adding schema for ' + schemaKey);
}
options.visited[schemaKey] = schema;
}
var otherTables = {};
var followingRelations = options.associations || options.relations;
if (followingRelations) {
// Handle foreign keys
var fks = {};
var foreignKeys = this.discoverForeignKeysSync(modelName, options);
foreignKeys.forEach(function(fk) {
var fkInfo = {
keySeq: fk.keySeq,
owner: fk.pkOwner,
tableName: fk.pkTableName,
columnName: fk.pkColumnName,
};
if (fks[fk.fkName]) {
fks[fk.fkName].push(fkInfo);
} else {
fks[fk.fkName] = [fkInfo];
}
});
if (self.settings.debug) {
debug('Foreign keys: ', fks);
}
schema.options.relations = {};
foreignKeys.forEach(function(fk) {
var propName = nameMapper('column', fk.pkTableName);
schema.options.relations[propName] = {
model: nameMapper('table', fk.pkTableName),
type: 'belongsTo',
foreignKey: nameMapper('column', fk.fkColumnName),
};
var key = fk.pkOwner + '.' + fk.pkTableName;
if (!options.visited.hasOwnProperty(key) && !otherTables.hasOwnProperty(key)) {
otherTables[key] = {owner: fk.pkOwner, tableName: fk.pkTableName};
}
});
}
if (Object.keys(otherTables).length === 0) {
return options.visited;
} else {
var moreTasks = [];
for (var t in otherTables) {
if (self.settings.debug) {
debug('Discovering related schema for ' + schemaKey);
}
var newOptions = {};
for (var key in options) {
newOptions[key] = options[key];
}
newOptions.owner = otherTables[t].owner;
self.discoverSchemasSync(otherTables[t].tableName, newOptions);
}
return options.visited;
}
};
/**
* Discover and build models from the specified owner/modelName.
*
* @param {String} modelName The model name.
* @options {Object} [options] Options; see below.
* @property {String} owner|schema Database owner or schema name.
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
* @property {Boolean} all True if all owners are included; false otherwise.
* @property {Boolean} views True if views are included; false otherwise.
* @param {Function} [cb] The callback function
*/
DataSource.prototype.discoverAndBuildModels = function(modelName, options, cb) {
var self = this;
options = options || {};
this.discoverSchemas(modelName, options, function(err, schemas) {
if (err) {
cb && cb(err, schemas);
return;
}
var schemaList = [];
for (var s in schemas) {
var schema = schemas[s];
if (options.base) {
schema.options = schema.options || {};
schema.options.base = options.base;
}
schemaList.push(schema);
}
var models = self.modelBuilder.buildModels(schemaList,
self.createModel.bind(self));
cb && cb(err, models);
});
};
/**
* Discover and build models from the given owner/modelName synchronously.
*
* @param {String} modelName The model name.
* @options {Object} [options] Options; see below.
* @property {String} owner|schema Database owner or schema name.
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
* @property {Boolean} all True if all owners are included; false otherwise.
* @property {Boolean} views True if views are included; false otherwise.
* @param {String} modelName The model name
* @param {Object} [options] The options
*/
DataSource.prototype.discoverAndBuildModelsSync = function(modelName, options) {
options = options || {};
var schemas = this.discoverSchemasSync(modelName, options);
var schemaList = [];
for (var s in schemas) {
var schema = schemas[s];
if (options.base) {
schema.options = schema.options || {};
schema.options.base = options.base;
}
schemaList.push(schema);
}
var models = this.modelBuilder.buildModels(schemaList,
this.createModel.bind(this));
return models;
};
/**
* Introspect a JSON object and build a model class
* @param {String} name Name of the model
* @param {Object} json The json object representing a model instance
* @param {Object} options Options
* @returns {*}
*/
DataSource.prototype.buildModelFromInstance = function(name, json, options) {
// Introspect the JSON document to generate a schema
var schema = ModelBuilder.introspect(json);
// Create a model for the generated schema
return this.createModel(name, schema, options);
};
/**
* Check whether migrations needed
* This method applies only to SQL connectors.
* @param {String|String[]} [models] A model name or an array of model names. If not present, apply to all models.
*/
DataSource.prototype.isActual = function(models, cb) {
this.freeze();
if (this.connector.isActual) {
this.connector.isActual(models, cb);
} else {
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
if (cb) {
process.nextTick(function() {
cb(null, true);
});
}
}
};
/**
* Log benchmarked message. Do not redefine this method, if you need to grab
* chema logs, use `dataSource.on('log', ...)` emitter event
*
* @private used by connectors
*/
DataSource.prototype.log = function(sql, t) {
debug(sql, t);
this.emit('log', sql, t);
};
/**
* Freeze dataSource. Behavior depends on connector
*/
DataSource.prototype.freeze = function freeze() {
if (!this.connector) {
throw new Error(g.f('The connector has not been initialized.'));
}
if (this.connector.freezeDataSource) {
this.connector.freezeDataSource();
}
if (this.connector.freezeSchema) {
this.connector.freezeSchema();
}
};
/**
* Return table name for specified `modelName`
* @param {String} modelName The model name
*/
DataSource.prototype.tableName = function(modelName) {
return this.getModelDefinition(modelName).tableName(this.connector.name);
};
/**
* Return column name for specified modelName and propertyName
* @param {String} modelName The model name
* @param {String} propertyName The property name
* @returns {String} columnName The column name.
*/
DataSource.prototype.columnName = function(modelName, propertyName) {
return this.getModelDefinition(modelName).columnName(this.connector.name, propertyName);
};
/**
* Return column metadata for specified modelName and propertyName
* @param {String} modelName The model name
* @param {String} propertyName The property name
* @returns {Object} column metadata
*/
DataSource.prototype.columnMetadata = function(modelName, propertyName) {
return this.getModelDefinition(modelName).columnMetadata(this.connector.name, propertyName);
};
/**
* Return column names for specified modelName
* @param {String} modelName The model name
* @returns {String[]} column names
*/
DataSource.prototype.columnNames = function(modelName) {
return this.getModelDefinition(modelName).columnNames(this.connector.name);
};
/**
* Find the ID column name
* @param {String} modelName The model name
* @returns {String} columnName for ID
*/
DataSource.prototype.idColumnName = function(modelName) {
return this.getModelDefinition(modelName).idColumnName(this.connector.name);
};
/**
* Find the ID property name
* @param {String} modelName The model name
* @returns {String} property name for ID
*/
DataSource.prototype.idName = function(modelName) {
if (!this.getModelDefinition(modelName).idName) {
g.error('No {{id}} name %s', this.getModelDefinition(modelName));
}
return this.getModelDefinition(modelName).idName();
};
/**
* Find the ID property names sorted by the index
* @param {String} modelName The model name
* @returns {String[]} property names for IDs
*/
DataSource.prototype.idNames = function(modelName) {
return this.getModelDefinition(modelName).idNames();
};
/**
* Find the id property definition
* @param {String} modelName The model name
* @returns {Object} The id property definition
*/
DataSource.prototype.idProperty = function(modelName) {
var def = this.getModelDefinition(modelName);
var idProps = def && def.ids();
return idProps && idProps[0] && idProps[0].property;
};
/**
* Define foreign key to another model
* @param {String} className The model name that owns the key
* @param {String} key Name of key field
* @param {String} foreignClassName The foreign model name
* @param {String} pkName (optional) primary key used for foreignKey
*/
DataSource.prototype.defineForeignKey = function defineForeignKey(className, key, foreignClassName, pkName) {
var pkType = null;
var foreignModel = this.getModelDefinition(foreignClassName);
pkName = pkName || foreignModel && foreignModel.idName();
if (pkName) {
pkType = foreignModel.properties[pkName].type;
}
var model = this.getModelDefinition(className);
if (model.properties[key]) {
if (pkType) {
// Reset the type of the foreign key
model.rawProperties[key].type = model.properties[key].type = pkType;
}
return;
}
var fkDef = {type: pkType};
var foreignMeta = this.columnMetadata(foreignClassName, pkName);
if (foreignMeta && (foreignMeta.dataType || foreignMeta.dataLength)) {
fkDef[this.connector.name] = {};
if (foreignMeta.dataType) {
fkDef[this.connector.name].dataType = foreignMeta.dataType;
}
if (foreignMeta.dataLength) {
fkDef[this.connector.name].dataLength = foreignMeta.dataLength;
}
}
if (this.connector.defineForeignKey) {
var cb = function(err, keyType) {
if (err) throw err;
fkDef.type = keyType || pkType;
// Add the foreign key property to the data source _models
this.defineProperty(className, key, fkDef);
}.bind(this);
switch (this.connector.defineForeignKey.length) {
case 4:
this.connector.defineForeignKey(className, key, foreignClassName, cb);
break;
default:
case 3:
this.connector.defineForeignKey(className, key, cb);
break;
}
} else {
// Add the foreign key property to the data source _models
this.defineProperty(className, key, fkDef);
}
};
/**
* Close database connection
* @param {Function} [cb] The callback function. Optional.
*/
DataSource.prototype.disconnect = function disconnect(cb) {
var self = this;
if (this.connected && (typeof this.connector.disconnect === 'function')) {
this.connector.disconnect(function(err, result) {
self.connected = false;
cb && cb(err, result);
});
} else {
process.nextTick(function() {
self.connected = false;
cb && cb();
});
}
};
/**
* Copy the model from Master.
* @param {Function} Master The model constructor
* @returns {Function} The copy of the model constructor
*
* @private
*/
DataSource.prototype.copyModel = function copyModel(Master) {
var dataSource = this;
var className = Master.modelName;
var md = Master.modelBuilder.getModelDefinition(className);
var Slave = function SlaveModel() {
Master.apply(this, [].slice.call(arguments));
};
util.inherits(Slave, Master);
// Delegating static properties
Slave.__proto__ = Master;
hiddenProperty(Slave, 'dataSource', dataSource);
hiddenProperty(Slave, 'modelName', className);
hiddenProperty(Slave, 'relations', Master.relations);
if (!(className in dataSource.modelBuilder.models)) {
// store class in model pool
dataSource.modelBuilder.models[className] = Slave;
dataSource.modelBuilder.definitions[className] =
new ModelDefinition(dataSource.modelBuilder, md.name, md.properties, md.settings);
if ((!dataSource.isTransaction) && dataSource.connector && dataSource.connector.define) {
dataSource.connector.define({
model: Slave,
properties: md.properties,
settings: md.settings,
});
}
}
return Slave;
};
/**
*
* @returns {EventEmitter}
* @private
*/
DataSource.prototype.transaction = function() {
var dataSource = this;
var transaction = new EventEmitter();
for (var p in dataSource) {
transaction[p] = dataSource[p];
}
transaction.isTransaction = true;
transaction.origin = dataSource;
transaction.name = dataSource.name;
transaction.settings = dataSource.settings;
transaction.connected = false;
transaction.connecting = false;
transaction.connector = dataSource.connector.transaction();
// create blank models pool
transaction.modelBuilder = new ModelBuilder();
transaction.models = transaction.modelBuilder.models;
transaction.definitions = transaction.modelBuilder.definitions;
for (var i in dataSource.modelBuilder.models) {
dataSource.copyModel.call(transaction, dataSource.modelBuilder.models[i]);
}
transaction.exec = function(cb) {
transaction.connector.exec(cb);
};
return transaction;
};
/**
* Enable remote access to a data source operation. Each [connector](#connector) has its own set of set
* remotely enabled and disabled operations. To list the operations, call `dataSource.operations()`.
* @param {String} operation The operation name
*/
DataSource.prototype.enableRemote = function(operation) {
var op = this.getOperation(operation);
if (op) {
op.remoteEnabled = true;
} else {
throw new Error(g.f('%s is not provided by the attached connector', operation));
}
};
/**
* Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled
* and disabled operations. To list the operations, call `dataSource.operations()`.
*
*```js
* var oracle = loopback.createDataSource({
* connector: require('loopback-connector-oracle'),
* host: '...',
* ...
* });
* oracle.disableRemote('destroyAll');
* ```
* **Notes:**
*
* - Disabled operations will not be added to attached models.
* - Disabling the remoting for a method only affects client access (it will still be available from server models).
* - Data sources must enable / disable operations before attaching or creating models.
* @param {String} operation The operation name
*/
DataSource.prototype.disableRemote = function(operation) {
var op = this.getOperation(operation);
if (op) {
op.remoteEnabled = false;
} else {
throw new Error(g.f('%s is not provided by the attached connector', operation));
}
};
/**
* Get an operation's metadata.
* @param {String} operation The operation name
*/
DataSource.prototype.getOperation = function(operation) {
var ops = this.operations();
var opKeys = Object.keys(ops);
for (var i = 0; i < opKeys.length; i++) {
var op = ops[opKeys[i]];
if (op.name === operation) {
return op;
}
}
};
/**
* Return JSON object describing all operations.
*
* Example return value:
* ```js
* {
* find: {
* remoteEnabled: true,
* accepts: [...],
* returns: [...]
* enabled: true
* },
* save: {
* remoteEnabled: true,
* prototype: true,
* accepts: [...],
* returns: [...],
* enabled: true
* },
* ...
* }
* ```
*/
DataSource.prototype.operations = function() {
return this._operations;
};
/**
* Define an operation to the data source
* @param {String} name The operation name
* @param {Object} options The options
* @param {Function} fn The function
*/
DataSource.prototype.defineOperation = function(name, options, fn) {
options.fn = fn;
options.name = name;
this._operations[name] = options;
};
/**
* Check if the backend is a relational DB
* @returns {Boolean}
*/
DataSource.prototype.isRelational = function() {
return this.connector && this.connector.relational;
};
/*!
* Check if the data source is ready.
* Returns a Boolean value.
* @param {Object} obj ?
* @param {Object} args ?
*/
DataSource.prototype.ready = function(obj, args) {
var self = this;
if (this.connected) {
// Connected
return false;
}
var method = args.callee;
// Set up a callback after the connection is established to continue the method call
var onConnected = null, onError = null, timeoutHandle = null;
onConnected = function() {
// Remove the error handler
self.removeListener('error', onError);
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
var params = [].slice.call(args);
try {
method.apply(obj, params);
} catch (err) {
// Catch the exception and report it via callback
var cb = params.pop();
if (typeof cb === 'function') {
process.nextTick(function() {
cb(err);
});
} else {
throw err;
}
}
};
onError = function(err) {
// Remove the connected listener
self.removeListener('connected', onConnected);
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
var params = [].slice.call(args);
var cb = params.pop();
if (typeof cb === 'function') {
process.nextTick(function() {
cb(err);
});
}
};
this.once('connected', onConnected);
this.once('error', onError);
// Set up a timeout to cancel the invocation
var timeout = this.settings.connectionTimeout || 5000;
timeoutHandle = setTimeout(function() {
self.removeListener('error', onError);
self.removeListener('connected', onConnected);
var params = [].slice.call(args);
var cb = params.pop();
if (typeof cb === 'function') {
cb(new Error(g.f('Timeout in connecting after %s ms', timeout)));
}
}, timeout);
if (!this.connecting) {
this.connect();
}
return true;
};
/**
* Ping the underlying connector to test the connections
* @param {Function} [cb] Callback function
*/
DataSource.prototype.ping = function(cb) {
var self = this;
if (self.connector.ping) {
this.connector.ping(cb);
} else if (self.connector.discoverModelProperties) {
self.discoverModelProperties('dummy', {}, cb);
} else {
process.nextTick(function() {
var err = self.connected ? null : new Error(g.f('Not connected'));
cb(err);
});
}
};
/**
* Define a hidden property
* @param {Object} obj The property owner
* @param {String} key The property name
* @param {Mixed} value The default value
*/
function hiddenProperty(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
enumerable: false,
configurable: false,
value: value,
});
}
/**
* Define readonly property on object
*
* @param {Object} obj The property owner
* @param {String} key The property name
* @param {Mixed} value The default value
*/
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
enumerable: true,
configurable: true,
value: value,
});
}
// Carry over a few properties/methods from the ModelBuilder as some tests use them
DataSource.Text = ModelBuilder.Text;
DataSource.JSON = ModelBuilder.JSON;
DataSource.Any = ModelBuilder.Any;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
/*!
* Get a near filter from a given where object. For connector use only.
*/
exports.nearFilter = function nearFilter(where) {
function nearSearch(clause, parentKeys) {
if (typeof clause !== 'object') {
return false;
}
parentKeys = parentKeys || [];
Object.keys(clause).forEach(function(clauseKey) {
if (Array.isArray(clause[clauseKey])) {
clause[clauseKey].forEach(function(el, index) {
var ret = nearSearch(el, parentKeys.concat(clauseKey).concat(index));
if (ret) return ret;
});
} else {
if (clause[clauseKey].hasOwnProperty('near')) {
var result = clause[clauseKey];
nearResults.push({
near: result.near,
maxDistance: result.maxDistance,
minDistance: result.minDistance,
unit: result.unit,
// If key is at root, define a single string, otherwise append it to the full path array
mongoKey: parentKeys.length ? parentKeys.concat(clauseKey) : clauseKey,
key: clauseKey,
});
}
}
});
}
var nearResults = [];
nearSearch(where);
return (!nearResults.length ? false : nearResults);
};
/*!
* Filter a set of results using the given filters returned by `nearFilter()`.
* Can support multiple locations, but will include results from all of them.
*
* WARNING: "or" operator with GeoPoint does not work as expected, eg:
* {where: {or: [{location: {near: (29,-90)}},{name:'Sean'}]}}
* Will actually work as if you had used "and". This is because geo filtering
* takes place outside of the SQL query, so the result set of "name = Sean" is
* returned by the database, and then the location filtering happens in the app
* logic. So the "near" operator is always an "and" of the SQL filters, and "or"
* of other GeoPoint filters.
*
* Additionally, since this step occurs after the SQL result set is returned,
* if using GeoPoints with pagination the result set may be smaller than the
* page size. The page size is enforced at the DB level, and then we may
* remove results at the Geo-app level. If we "limit: 25", but 4 of those results
* do not have a matching geopoint field, the request will only return 21 results.
* This may make it erroneously look like a given page is the end of the result set.
*/
exports.filter = function(rawResults, filters) {
var distances = {};
var results = [];
filters.forEach(function(filter) {
var origin = filter.near;
var max = filter.maxDistance > 0 ? filter.maxDistance : false;
var min = filter.minDistance > 0 ? filter.minDistance : false;
var unit = filter.unit;
var key = filter.key;
// create distance index
rawResults.forEach(function(result) {
var loc = result[key];
// filter out results without locations
if (!loc) return;
if (!(loc instanceof GeoPoint)) loc = GeoPoint(loc);
if (typeof loc.lat !== 'number') return;
if (typeof loc.lng !== 'number') return;
var d = GeoPoint.distanceBetween(origin, loc, {type: unit});
// filter result if distance is either < minDistance or > maxDistance
if ((min && d < min) || (max && d > max)) return;
distances[result.id] = d;
results.push(result);
});
results.sort(function(resA, resB) {
var a = resA[key];
var b = resB[key];
if (a && b) {
var da = distances[resA.id];
var db = distances[resB.id];
if (db === da) return 0;
return da > db ? 1 : -1;
} else {
return 0;
}
});
});
return results;
};
exports.GeoPoint = GeoPoint;
/**
* The GeoPoint object represents a physical location.
*
* For example:
*
* ```js
* var loopback = require(‘loopback’);
* var here = new loopback.GeoPoint({lat: 10.32424, lng: 5.84978});
* ```
*
* Embed a latitude / longitude point in a model.
*
* ```js
* var CoffeeShop = loopback.createModel('coffee-shop', {
* location: 'GeoPoint'
* });
* ```
*
* You can query LoopBack models with a GeoPoint property and an attached data source using geo-spatial filters and
* sorting. For example, the following code finds the three nearest coffee shops.
*
* ```js
* CoffeeShop.attachTo(oracle);
* var here = new GeoPoint({lat: 10.32424, lng: 5.84978});
* CoffeeShop.find( {where: {location: {near: here}}, limit:3}, function(err, nearbyShops) {
* console.info(nearbyShops); // [CoffeeShop, ...]
* });
* ```
* @class GeoPoint
* @property {Number} lat The latitude in degrees.
* @property {Number} lng The longitude in degrees.
*
* @options {Object} Options Object with two Number properties: lat and long.
* @property {Number} lat The latitude point in degrees. Range: -90 to 90.
* @property {Number} lng The longitude point in degrees. Range: -180 to 180.
*
* @options {Array} Options Array with two Number entries: [lat,long].
* @property {Number} lat The latitude point in degrees. Range: -90 to 90.
* @property {Number} lng The longitude point in degrees. Range: -180 to 180.
*/
function GeoPoint(data) {
if (!(this instanceof GeoPoint)) {
return new GeoPoint(data);
}
if (arguments.length === 2) {
data = {
lat: arguments[0],
lng: arguments[1],
};
}
assert(Array.isArray(data) || typeof data === 'object' || typeof data === 'string',
'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string');
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (err) {
data = data.split(/,\s*/);
assert(data.length === 2, 'must provide a string "lat,lng" creating a GeoPoint with a string');
}
}
if (Array.isArray(data)) {
data = {
lat: Number(data[0]),
lng: Number(data[1]),
};
} else {
data.lng = Number(data.lng);
data.lat = Number(data.lat);
}
assert(typeof data === 'object', 'must provide a lat and lng object when creating a GeoPoint');
assert(typeof data.lat === 'number' && !isNaN(data.lat), 'lat must be a number when creating a GeoPoint');
assert(typeof data.lng === 'number' && !isNaN(data.lng), 'lng must be a number when creating a GeoPoint');
assert(data.lng <= 180, 'lng must be <= 180');
assert(data.lng >= -180, 'lng must be >= -180');
assert(data.lat <= 90, 'lat must be <= 90');
assert(data.lat >= -90, 'lat must be >= -90');
this.lat = data.lat;
this.lng = data.lng;
}
/**
* Determine the spherical distance between two GeoPoints.
*
* @param {GeoPoint} pointA Point A
* @param {GeoPoint} pointB Point B
* @options {Object} options Options object with one key, 'type'. See below.
* @property {String} type Unit of measurement, one of:
*
* - `miles` (default)
* - `radians`
* - `kilometers`
* - `meters`
* - `miles`
* - `feet`
* - `degrees`
*/
GeoPoint.distanceBetween = function distanceBetween(a, b, options) {
if (!(a instanceof GeoPoint)) {
a = GeoPoint(a);
}
if (!(b instanceof GeoPoint)) {
b = GeoPoint(b);
}
var x1 = a.lat;
var y1 = a.lng;
var x2 = b.lat;
var y2 = b.lng;
return geoDistance(x1, y1, x2, y2, options);
};
/**
* Determine the spherical distance to the given point.
* Example:
* ```js
* var loopback = require(‘loopback’);
*
* var here = new loopback.GeoPoint({lat: 10, lng: 10});
* var there = new loopback.GeoPoint({lat: 5, lng: 5});
*
* loopback.GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
* ```
* @param {Object} point GeoPoint object to which to measure distance.
* @options {Object} options Options object with one key, 'type'. See below.
* @property {String} type Unit of measurement, one of:
*
* - `miles` (default)
* - `radians`
* - `kilometers`
* - `meters`
* - `miles`
* - `feet`
* - `degrees`
*/
GeoPoint.prototype.distanceTo = function(point, options) {
return GeoPoint.distanceBetween(this, point, options);
};
/**
* Simple serialization.
*/
GeoPoint.prototype.toString = function() {
return this.lat + ',' + this.lng;
};
/**
* @property {Number} DEG2RAD - Factor to convert degrees to radians.
* @property {Number} RAD2DEG - Factor to convert radians to degrees.
* @property {Object} EARTH_RADIUS - Radius of the earth.
*/
// factor to convert degrees to radians
var DEG2RAD = 0.01745329252;
// factor to convert radians degrees to degrees
var RAD2DEG = 57.29577951308;
// radius of the earth
var EARTH_RADIUS = {
kilometers: 6370.99056,
meters: 6370990.56,
miles: 3958.75,
feet: 20902200,
radians: 1,
degrees: RAD2DEG,
};
function geoDistance(x1, y1, x2, y2, options) {
var type = (options && options.type) || 'miles';
// Convert to radians
x1 = x1 * DEG2RAD;
y1 = y1 * DEG2RAD;
x2 = x2 * DEG2RAD;
y2 = y2 * DEG2RAD;
// use the haversine formula to calculate distance for any 2 points on a sphere.
// ref http://en.wikipedia.org/wiki/Haversine_formula
var haversine = function(a) {
return Math.pow(Math.sin(a / 2.0), 2);
};
var f = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1));
return 2 * Math.asin(f) * EARTH_RADIUS[type];
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var deprecated = require('depd')('loopback-datasource-juggler');
var g = require('strong-globalize')();
/*!
* Module exports
*/
module.exports = Hookable;
/*
* Hooks object.
* @class Hookable
*/
function Hookable() {
}
/**
* List of hooks available
*/
Hookable.afterInitialize = null;
Hookable.beforeValidate = null;
Hookable.afterValidate = null;
Hookable.beforeSave = null;
Hookable.afterSave = null;
Hookable.beforeCreate = null;
Hookable.afterCreate = null;
Hookable.beforeUpdate = null;
Hookable.afterUpdate = null;
Hookable.beforeDestroy = null;
Hookable.afterDestroy = null;
// TODO: Evaluate https://github.com/bnoguchi/hooks-js/
Hookable.prototype.trigger = function trigger(actionName, work, data, callback) {
var capitalizedName = capitalize(actionName);
var beforeHook = this.constructor['before' + capitalizedName] ||
this.constructor['pre' + capitalizedName];
var afterHook = this.constructor['after' + capitalizedName] ||
this.constructor['post' + capitalizedName];
Iif (actionName === 'validate') {
beforeHook = beforeHook || this.constructor.beforeValidation;
afterHook = afterHook || this.constructor.afterValidation;
}
var inst = this;
Iif (actionName !== 'initialize') {
if (beforeHook)
deprecateHook(inst.constructor, ['before', 'pre'], capitalizedName);
if (afterHook)
deprecateHook(inst.constructor, ['after', 'post'], capitalizedName);
}
// we only call "before" hook when we have actual action (work) to perform
Iif (work) {
if (beforeHook) {
// before hook should be called on instance with two parameters: next and data
beforeHook.call(inst, function() {
// Check arguments to next(err, result)
if (arguments.length) {
return callback && callback.apply(null, arguments);
}
// No err & result is present, proceed with the real work
// actual action also have one param: callback
work.call(inst, next);
}, data);
} else {
work.call(inst, next);
}
} else {
next();
}
function next(done) {
Iif (afterHook) {
afterHook.call(inst, done);
} else Iif (done) {
done.call(this);
}
}
};
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function deprecateHook(ctor, prefixes, capitalizedName) {
var candidateNames = prefixes.map(function(p) { return p + capitalizedName; });
if (capitalizedName === 'Validate')
candidateNames.push(prefixes[0] + 'Validation');
var hookName = candidateNames.filter(function(hook) { return !!ctor[hook]; })[0];
if (!hookName) return; // just to be sure, this should never happen
if (ctor.modelName) hookName = ctor.modelName + '.' + hookName;
deprecated(g.f('Model hook "%s" is deprecated, ' +
'use Operation hooks instead. ' +
'{{http://docs.strongloop.com/display/LB/Operation+hooks}}', hookName));
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var async = require('async');
var g = require('strong-globalize')();
var utils = require('./utils');
var List = require('./list');
var includeUtils = require('./include_utils');
var isPlainObject = utils.isPlainObject;
var defineCachedRelations = utils.defineCachedRelations;
var uniq = utils.uniq;
var idName = utils.idName;
/*!
* Normalize the include to be an array
* @param include
* @returns {*}
*/
function normalizeInclude(include) {
var newInclude;
if (typeof include === 'string') {
return [include];
} else if (isPlainObject(include)) {
// Build an array of key/value pairs
newInclude = [];
var rel = include.rel || include.relation;
var obj = {};
if (typeof rel === 'string') {
obj[rel] = new IncludeScope(include.scope);
newInclude.push(obj);
} else {
for (var key in include) {
obj[key] = include[key];
newInclude.push(obj);
}
}
return newInclude;
} else if (Array.isArray(include)) {
newInclude = [];
for (var i = 0, n = include.length; i < n; i++) {
var subIncludes = normalizeInclude(include[i]);
newInclude = newInclude.concat(subIncludes);
}
return newInclude;
} else {
return include;
}
}
function IncludeScope(scope) {
this._scope = utils.deepMerge({}, scope || {});
if (this._scope.include) {
this._include = normalizeInclude(this._scope.include);
delete this._scope.include;
} else {
this._include = null;
}
}
IncludeScope.prototype.conditions = function() {
return utils.deepMerge({}, this._scope);
};
IncludeScope.prototype.include = function() {
return this._include;
};
/*!
* Look up a model by name from the list of given models
* @param {Object} models Models keyed by name
* @param {String} modelName The model name
* @returns {*} The matching model class
*/
function lookupModel(models, modelName) {
if (models[modelName]) {
return models[modelName];
}
var lookupClassName = modelName.toLowerCase();
for (var name in models) {
if (name.toLowerCase() === lookupClassName) {
return models[name];
}
}
}
/**
* Utility Function to allow interleave before and after high computation tasks
* @param tasks
* @param callback
*/
function execTasksWithInterLeave(tasks, callback) {
// let's give others some time to process.
// Context Switch BEFORE Heavy Computation
process.nextTick(function() {
// Heavy Computation
try {
async.parallel(tasks, function(err, info) {
// Context Switch AFTER Heavy Computation
process.nextTick(function() {
callback(err, info);
});
});
} catch (err) {
callback(err);
}
});
}
/*!
* Include mixin for ./model.js
*/
module.exports = Inclusion;
/**
* Inclusion - Model mixin.
*
* @class
*/
function Inclusion() {
}
/**
* Normalize includes - used in DataAccessObject
*
*/
Inclusion.normalizeInclude = normalizeInclude;
/**
* Enables you to load relations of several objects and optimize numbers of requests.
*
* Examples:
*
* Load all users' posts with only one additional request:
* `User.include(users, 'posts', function() {});`
* Or
* `User.include(users, ['posts'], function() {});`
*
* Load all users posts and passports with two additional requests:
* `User.include(users, ['posts', 'passports'], function() {});`
*
* Load all passports owner (users), and all posts of each owner loaded:
*```Passport.include(passports, {owner: 'posts'}, function() {});
*``` Passport.include(passports, {owner: ['posts', 'passports']});
*``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']});
*
* @param {Array} objects Array of instances
* @param {String|Object|Array} include Which relations to load.
* @param {Object} [options] Options for CRUD
* @param {Function} cb Callback called when relations are loaded
*
*/
Inclusion.include = function(objects, include, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
if (!include || (Array.isArray(include) && include.length === 0) ||
(Array.isArray(objects) && objects.length === 0) ||
(isPlainObject(include) && Object.keys(include).length === 0)) {
// The objects are empty
return process.nextTick(function() {
cb && cb(null, objects);
});
}
include = normalizeInclude(include);
// Find the limit of items for `inq`
var inqLimit = 256;
if (self.dataSource && self.dataSource.settings &&
self.dataSource.settings.inqLimit) {
inqLimit = self.dataSource.settings.inqLimit;
}
async.each(include, function(item, callback) {
processIncludeItem(objects, item, options, callback);
}, function(err) {
cb && cb(err, objects);
});
/**
* Find related items with an array of foreign keys by page
* @param model The model class
* @param filter The query filter
* @param fkName The name of the foreign key property
* @param pageSize The size of page
* @param options Options
* @param cb
*/
function findWithForeignKeysByPage(model, filter, fkName, pageSize, options, cb) {
var foreignKeys = [];
if (filter.where[fkName]) {
foreignKeys = filter.where[fkName].inq;
} else if (filter.where.and) {
// The inq can be embedded inside 'and: []'. No or: [] is needed as
// include only uses and. We only deal with the generated inq for include.
for (var j in filter.where.and) {
if (filter.where.and[j][fkName] &&
Array.isArray(filter.where.and[j][fkName].inq)) {
foreignKeys = filter.where.and[j][fkName].inq;
break;
}
}
}
if (!foreignKeys.length) {
return cb(null, []);
}
if (filter.limit || filter.skip || filter.offset) {
// Force the find to be performed per FK to honor the pagination
pageSize = 1;
}
var size = foreignKeys.length;
if (size > inqLimit && pageSize <= 0) {
pageSize = inqLimit;
}
if (pageSize <= 0) {
return model.find(filter, options, cb);
}
var listOfFKs = [];
for (var i = 0; i < size; i += pageSize) {
var end = i + pageSize;
if (end > size) {
end = size;
}
listOfFKs.push(foreignKeys.slice(i, end));
}
var items = [];
// Optimization: no need to resolve keys that are an empty array
listOfFKs = listOfFKs.filter(function(keys) {
return keys.length > 0;
});
async.each(listOfFKs, function(foreignKeys, done) {
var newFilter = {};
for (var f in filter) {
newFilter[f] = filter[f];
}
if (filter.where) {
newFilter.where = {};
for (var w in filter.where) {
newFilter.where[w] = filter.where[w];
}
}
newFilter.where[fkName] = {
inq: foreignKeys,
};
model.find(newFilter, options, function(err, results) {
if (err) return done(err);
items = items.concat(results);
done();
});
}, function(err) {
if (err) return cb(err);
cb(null, items);
});
}
function processIncludeItem(objs, include, options, cb) {
var relations = self.relations;
var relationName;
var subInclude = null, scope = null;
if (isPlainObject(include)) {
relationName = Object.keys(include)[0];
if (include[relationName] instanceof IncludeScope) {
scope = include[relationName];
subInclude = scope.include();
} else {
subInclude = include[relationName];
// when include = {user:true}, it does not have subInclude
if (subInclude === true) {
subInclude = null;
}
}
} else {
relationName = include;
subInclude = null;
}
var relation = relations[relationName];
if (!relation) {
cb(new Error(g.f('Relation "%s" is not defined for %s model', relationName, self.modelName)));
return;
}
var polymorphic = relation.polymorphic;
// if (polymorphic && !polymorphic.discriminator) {
// cb(new Error('Relation "' + relationName + '" is polymorphic but ' +
// 'discriminator is not present'));
// return;
// }
if (!relation.modelTo) {
if (!relation.polymorphic) {
cb(new Error(g.f('{{Relation.modelTo}} is not defined for relation %s and is no {{polymorphic}}',
relationName)));
return;
}
}
// Just skip if inclusion is disabled
if (relation.options.disableInclude) {
return cb();
}
// prepare filter and fields for making DB Call
var filter = (scope && scope.conditions()) || {};
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
var includeScope = {};
// make sure not to miss any fields for sub includes
if (filter.fields && Array.isArray(subInclude) &&
relation.modelTo.relations) {
includeScope.fields = [];
subInclude.forEach(function(name) {
var rel = relation.modelTo.relations[name];
if (rel && rel.type === 'belongsTo') {
includeScope.fields.push(rel.keyFrom);
}
});
}
utils.mergeQuery(filter, includeScope, {fields: false});
}
// Let's add a placeholder where query
filter.where = filter.where || {};
// if fields are specified, make sure target foreign key is present
var fields = filter.fields;
if (typeof fields === 'string') {
// transform string into array containing this string
filter.fields = fields = [fields];
}
if (Array.isArray(fields) && fields.indexOf(relation.keyTo) === -1) {
fields.push(relation.keyTo);
} else if (isPlainObject(fields) && !fields[relation.keyTo]) {
fields[relation.keyTo] = true;
}
//
// call relation specific include functions
//
if (relation.multiple) {
if (relation.modelThrough) {
// hasManyThrough needs separate handling
return includeHasManyThrough(cb);
}
// This will also include embedsMany with belongsTo.
// Might need to optimize db calls for this.
if (relation.type === 'embedsMany') {
// embedded docs are part of the objects, no need to make db call.
// proceed as implemented earlier.
return includeEmbeds(cb);
}
if (relation.type === 'referencesMany') {
return includeReferencesMany(cb);
}
// This handles exactly hasMany. Fast and straightforward. Without parallel, each and other boilerplate.
if (relation.type === 'hasMany' && relation.multiple && !subInclude) {
return includeHasManySimple(cb);
}
// assuming all other relations with multiple=true as hasMany
return includeHasMany(cb);
} else {
if (polymorphic) {
if (relation.type === 'hasOne') {
return includePolymorphicHasOne(cb);
}
return includePolymorphicBelongsTo(cb);
}
if (relation.type === 'embedsOne') {
return includeEmbeds(cb);
}
// hasOne or belongsTo
return includeOneToOne(cb);
}
/**
* Handle inclusion of HasManyThrough/HasAndBelongsToMany/Polymorphic
* HasManyThrough relations
* @param callback
*/
function includeHasManyThrough(callback) {
var sourceIds = [];
// Map for Indexing objects by their id for faster retrieval
var objIdMap = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
// one-to-many: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
var sourceId = obj[relation.keyFrom];
if (sourceId) {
sourceIds.push(sourceId);
objIdMap[sourceId.toString()] = obj;
}
// sourceId can be null. but cache empty data as result
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = [];
}
// default filters are not applicable on through model. should be applied
// on modelTo later in 2nd DB call.
var throughFilter = {
where: {},
};
throughFilter.where[relation.keyTo] = {
inq: uniq(sourceIds),
};
if (polymorphic) {
// handle polymorphic hasMany (reverse) in which case we need to filter
// by discriminator to filter other types
throughFilter.where[polymorphic.discriminator] =
relation.modelFrom.definition.name;
}
/**
* 1st DB Call of 2 step process. Get through model objects first
*/
findWithForeignKeysByPage(relation.modelThrough, throughFilter,
relation.keyTo, 0, options, throughFetchHandler);
/**
* Handle the results of Through model objects and fetch the modelTo items
* @param err
* @param {Array<Model>} throughObjs
* @returns {*}
*/
function throughFetchHandler(err, throughObjs) {
if (err) {
return callback(err);
}
// start preparing for 2nd DB call.
var targetIds = [];
var targetObjsMap = {};
for (var i = 0; i < throughObjs.length; i++) {
var throughObj = throughObjs[i];
var targetId = throughObj[relation.keyThrough];
if (targetId) {
// save targetIds for 2nd DB Call
targetIds.push(targetId);
var sourceObj = objIdMap[throughObj[relation.keyTo]];
var targetIdStr = targetId.toString();
// Since targetId can be duplicates, multiple source objs are put
// into buckets.
var objList = targetObjsMap[targetIdStr] =
targetObjsMap[targetIdStr] || [];
objList.push(sourceObj);
}
}
// Polymorphic relation does not have idKey of modelTo. Find it manually
var modelToIdName = idName(relation.modelTo);
filter.where[modelToIdName] = {
inq: uniq(targetIds),
};
// make sure that the modelToIdName is included if fields are specified
if (Array.isArray(fields) && fields.indexOf(modelToIdName) === -1) {
fields.push(modelToIdName);
} else if (isPlainObject(fields) && !fields[modelToIdName]) {
fields[modelToIdName] = true;
}
/**
* 2nd DB Call of 2 step process. Get modelTo (target) objects
*/
findWithForeignKeysByPage(relation.modelTo, filter,
modelToIdName, 0, options, targetsFetchHandler);
// relation.modelTo.find(filter, options, targetsFetchHandler);
function targetsFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var tasks = [];
// simultaneously process subIncludes. Call it first as it is an async
// process.
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process & link each target with object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkManyToMany, next);
function linkManyToMany(target, next) {
var targetId = target[modelToIdName];
if (!targetId) {
var err = new Error(g.f('LinkManyToMany received target that doesn\'t contain required "%s"',
modelToIdName));
return next(err);
}
var objList = targetObjsMap[targetId.toString()];
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName].push(target);
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
}
/**
* Handle inclusion of ReferencesMany relation
* @param callback
*/
function includeReferencesMany(callback) {
var modelToIdName = idName(relation.modelTo);
var allTargetIds = [];
// Map for Indexing objects by their id for faster retrieval
var targetObjsMap = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
// one-to-many: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
var targetIds = obj[relation.keyFrom];
if (targetIds) {
if (typeof targetIds === 'string') {
// For relational DBs, the array is stored as stringified json
// Please note obj is a plain object at this point
targetIds = JSON.parse(targetIds);
}
// referencesMany has multiple targetIds per obj. We need to concat
// them into allTargetIds before DB Call
allTargetIds = allTargetIds.concat(targetIds);
for (var j = 0; j < targetIds.length; j++) {
var targetId = targetIds[j];
var targetIdStr = targetId.toString();
var objList = targetObjsMap[targetIdStr] =
targetObjsMap[targetIdStr] || [];
objList.push(obj);
}
}
// sourceId can be null. but cache empty data as result
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = [];
}
filter.where[relation.keyTo] = {
inq: uniq(allTargetIds),
};
relation.applyScope(null, filter);
/**
* Make the DB Call, fetch all target objects
*/
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Handle the fetched target objects
* @param err
* @param {Array<Model>}targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
targets = utils.sortObjectsByIds(modelToIdName, allTargetIds, targets);
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkManyToMany, next);
function linkManyToMany(target, next) {
var objList = targetObjsMap[target[relation.keyTo].toString()];
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName].push(target);
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
/**
* Handle inclusion of HasMany relation
* @param callback
*/
function includeHasManySimple(callback) {
// Map for Indexing objects by their id for faster retrieval
var objIdMap2 = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, relation.keyFrom);
filter.where[relation.keyTo] = {
inq: uniq(objIdMap2.getKeys()),
};
relation.applyScope(null, filter);
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var targetsIdMap = includeUtils.buildOneToManyIdentityMapWithOrigKeys(targets, relation.keyTo);
includeUtils.join(objIdMap2, targetsIdMap, function(obj1, valueToMergeIn) {
defineCachedRelations(obj1);
obj1.__cachedRelations[relationName] = valueToMergeIn;
processTargetObj(obj1, function() {});
});
callback(err, objs);
}
}
/**
* Handle inclusion of HasMany relation
* @param callback
*/
function includeHasMany(callback) {
var sourceIds = [];
// Map for Indexing objects by their id for faster retrieval
var objIdMap = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
// one-to-many: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
var sourceId = obj[relation.keyFrom];
if (sourceId) {
sourceIds.push(sourceId);
objIdMap[sourceId.toString()] = obj;
}
// sourceId can be null. but cache empty data as result
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = [];
}
filter.where[relation.keyTo] = {
inq: uniq(sourceIds),
};
relation.applyScope(null, filter);
options.partitionBy = relation.keyTo;
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
if (targets.length === 0) {
return async.each(objs, function(obj, next) {
processTargetObj(obj, next);
}, next);
}
async.each(targets, linkManyToOne, next);
function linkManyToOne(target, next) {
// fix for bug in hasMany with referencesMany
var targetIds = [].concat(target[relation.keyTo]);
async.each(targetIds, function(targetId, next) {
var obj = objIdMap[targetId.toString()];
if (!obj) return next();
obj.__cachedRelations[relationName].push(target);
processTargetObj(obj, next);
}, function(err, processedTargets) {
if (err) {
return next(err);
}
var objsWithEmptyRelation = objs.filter(function(obj) {
return obj.__cachedRelations[relationName].length === 0;
});
async.each(objsWithEmptyRelation, function(obj, next) {
processTargetObj(obj, next);
}, function(err) {
next(err, processedTargets);
});
});
}
}
execTasksWithInterLeave(tasks, callback);
}
}
/**
* Handle Inclusion of Polymorphic BelongsTo relation
* @param callback
*/
function includePolymorphicBelongsTo(callback) {
var targetIdsByType = {};
// Map for Indexing objects by their type and targetId for faster retrieval
var targetObjMapByType = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
var discriminator = polymorphic.discriminator;
var modelType = obj[discriminator];
if (modelType) {
targetIdsByType[modelType] = targetIdsByType[modelType] || [];
targetObjMapByType[modelType] = targetObjMapByType[modelType] || {};
var targetIds = targetIdsByType[modelType];
var targetObjsMap = targetObjMapByType[modelType];
var targetId = obj[relation.keyFrom];
if (targetId) {
targetIds.push(targetId);
var targetIdStr = targetId.toString();
targetObjsMap[targetIdStr] = targetObjsMap[targetIdStr] || [];
// Is belongsTo. Multiple objects can have the same
// targetId and therefore map value is an array
targetObjsMap[targetIdStr].push(obj);
}
}
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = null;
}
async.each(Object.keys(targetIdsByType), processPolymorphicType,
callback);
/**
* Process Polymorphic objects of each type (modelType)
* @param {String} modelType
* @param callback
*/
function processPolymorphicType(modelType, callback) {
var typeFilter = {where: {}};
utils.mergeQuery(typeFilter, filter);
var targetIds = targetIdsByType[modelType];
typeFilter.where[relation.keyTo] = {
inq: uniq(targetIds),
};
var Model = lookupModel(relation.modelFrom.dataSource.modelBuilder.
models, modelType);
if (!Model) {
callback(new Error(g.f('Discriminator type %s specified but no model exists with such name',
modelType)));
return;
}
relation.applyScope(null, typeFilter);
findWithForeignKeysByPage(Model, typeFilter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
Model.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
var targetObjsMap = targetObjMapByType[modelType];
async.each(targets, linkOneToMany, next);
function linkOneToMany(target, next) {
var objList = targetObjsMap[target[relation.keyTo].toString()];
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName] = target;
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
}
/**
* Handle Inclusion of Polymorphic HasOne relation
* @param callback
*/
function includePolymorphicHasOne(callback) {
var sourceIds = [];
// Map for Indexing objects by their id for faster retrieval
var objIdMap = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
// one-to-one: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
var sourceId = obj[relation.keyFrom];
if (sourceId) {
sourceIds.push(sourceId);
objIdMap[sourceId.toString()] = obj;
}
// sourceId can be null. but cache empty data as result
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = null;
}
filter.where[relation.keyTo] = {
inq: uniq(sourceIds),
};
relation.applyScope(null, filter);
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkOneToOne, next);
function linkOneToOne(target, next) {
var sourceId = target[relation.keyTo];
if (!sourceId) return next();
var obj = objIdMap[sourceId.toString()];
if (!obj) return next();
obj.__cachedRelations[relationName] = target;
processTargetObj(obj, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
/**
* Handle Inclusion of BelongsTo/HasOne relation
* @param callback
*/
function includeOneToOne(callback) {
var targetIds = [];
var objTargetIdMap = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
if (relation.type === 'belongsTo') {
if (obj[relation.keyFrom] === null ||
obj[relation.keyFrom] === undefined) {
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = null;
continue;
}
}
var targetId = obj[relation.keyFrom];
if (targetId) {
targetIds.push(targetId);
var targetIdStr = targetId.toString();
objTargetIdMap[targetIdStr] = objTargetIdMap[targetIdStr] || [];
objTargetIdMap[targetIdStr].push(obj);
}
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = null;
}
filter.where[relation.keyTo] = {
inq: uniq(targetIds),
};
relation.applyScope(null, filter);
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
var tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkOneToMany, next);
function linkOneToMany(target, next) {
var targetId = target[relation.keyTo];
var objList = objTargetIdMap[targetId.toString()];
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName] = target;
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
/**
* Handle Inclusion of EmbedsMany/EmbedsManyWithBelongsTo/EmbedsOne
* Relations. Since Embedded docs are part of parents, no need to make
* db calls. Let the related function be called for each object to fetch
* the results from cache.
*
* TODO: Optimize EmbedsManyWithBelongsTo relation DB Calls
* @param callback
*/
function includeEmbeds(callback) {
async.each(objs, function(obj, next) {
processTargetObj(obj, next);
}, callback);
}
/**
* Process Each Model Object and make sure specified relations are included
* @param {Model} obj - Single Mode object for which inclusion is needed
* @param callback
* @returns {*}
*/
function processTargetObj(obj, callback) {
var isInst = obj instanceof self;
// Calling the relation method on the instance
if (relation.type === 'belongsTo') {
// If the belongsTo relation doesn't have an owner
if (obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
defineCachedRelations(obj);
// Set to null if the owner doesn't exist
obj.__cachedRelations[relationName] = null;
if (isInst) {
obj.__data[relationName] = null;
} else {
obj[relationName] = null;
}
return callback();
}
}
/**
* Sets the related objects as a property of Parent Object
* @param {Array<Model>|Model|null} result - Related Object/Objects
* @param cb
*/
function setIncludeData(result, cb) {
if (isInst) {
if (Array.isArray(result) && !(result instanceof List)) {
result = new List(result, relation.modelTo);
}
obj.__data[relationName] = result;
// obj.setStrict(false); issue #1252
} else {
obj[relationName] = result;
}
cb(null, result);
}
// obj.__cachedRelations[relationName] can be null if no data was returned
if (obj.__cachedRelations &&
obj.__cachedRelations[relationName] !== undefined) {
return setIncludeData(obj.__cachedRelations[relationName],
callback);
}
var inst = (obj instanceof self) ? obj : new self(obj);
// If related objects are not cached by include Handlers, directly call
// related accessor function even though it is not very efficient
var related; // relation accessor function
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
var includeScope = {};
var filter = scope.conditions();
// make sure not to miss any fields for sub includes
if (filter.fields && Array.isArray(subInclude) && relation.modelTo.relations) {
includeScope.fields = [];
subInclude.forEach(function(name) {
var rel = relation.modelTo.relations[name];
if (rel && rel.type === 'belongsTo') {
includeScope.fields.push(rel.keyFrom);
}
});
}
utils.mergeQuery(filter, includeScope, {fields: false});
related = inst[relationName].bind(inst, filter);
} else {
related = inst[relationName].bind(inst, undefined);
}
related(options, function(err, result) {
if (err) {
return callback(err);
} else {
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = result;
return setIncludeData(result, callback);
}
});
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
module.exports.buildOneToOneIdentityMapWithOrigKeys = buildOneToOneIdentityMapWithOrigKeys;
module.exports.buildOneToManyIdentityMapWithOrigKeys = buildOneToManyIdentityMapWithOrigKeys;
module.exports.join = join;
module.exports.KVMap = KVMap;
/**
* Effectively builds associative map on id -> object relation and stores original keys.
* Map returned in form of object with ids in keys and object as values.
* @param objs array of objects to build from
* @param idName name of property to be used as id. Such property considered to be unique across array.
* In case of collisions last wins. For non-unique ids use buildOneToManyIdentityMap()
* @returns {} object where keys are ids and values are objects itself
*/
function buildOneToOneIdentityMapWithOrigKeys(objs, idName) {
var kvMap = new KVMap();
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
var id = obj[idName];
kvMap.set(id, obj);
}
return kvMap;
}
function buildOneToManyIdentityMapWithOrigKeys(objs, idName) {
var kvMap = new KVMap();
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
var id = obj[idName];
var value = kvMap.get(id) || [];
value.push(obj);
kvMap.set(id, value);
}
return kvMap;
}
/**
* Yeah, it joins. You need three things id -> obj1 map, id -> [obj2] map and merge function.
* This functions will take each obj1, locate all data to join in map2 and call merge function.
* @param oneToOneIdMap
* @param oneToManyIdMap
* @param mergeF function(obj, objectsToMergeIn)
*/
function join(oneToOneIdMap, oneToManyIdMap, mergeF) {
var ids = oneToOneIdMap.getKeys();
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var obj = oneToOneIdMap.get(id);
var objectsToMergeIn = oneToManyIdMap.get(id) || [];
mergeF(obj, objectsToMergeIn);
}
}
/**
* Map with arbitrary keys and values. User .set() and .get() to work with values instead of []
* @returns {{set: Function, get: Function, remove: Function, exist: Function, getKeys: Function}}
* @constructor
*/
function KVMap() {
var _originalKeyFieldName = 'originalKey';
var _valueKeyFieldName = 'value';
var _dict = {};
var keyToString = function(key) { return key.toString(); };
var mapImpl = {
set: function(key, value) {
var recordObj = {};
recordObj[_originalKeyFieldName] = key;
recordObj[_valueKeyFieldName] = value;
_dict[keyToString(key)] = recordObj;
return true;
},
get: function(key) {
var storeObj = _dict[keyToString(key)];
if (storeObj) {
return storeObj[_valueKeyFieldName];
} else {
return undefined;
}
},
remove: function(key) {
delete _dict[keyToString(key)];
return true;
},
exist: function(key) {
var result = _dict.hasOwnProperty(keyToString(key));
return result;
},
getKeys: function() {
var result = [];
for (var key in _dict) {
result.push(_dict[key][_originalKeyFieldName]);
}
return result;
},
};
return mapImpl;
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
module.exports = function getIntrospector(ModelBuilder) {
function introspectType(value) {
// Unknown type, using Any
if (value === null || value === undefined) {
return ModelBuilder.Any;
}
// Check registered schemaTypes
for (var t in ModelBuilder.schemaTypes) {
var st = ModelBuilder.schemaTypes[t];
if (st !== Object && st !== Array && (value instanceof st)) {
return t;
}
}
var type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return type;
}
if (value instanceof Date) {
return 'date';
}
var itemType;
if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) {
if (value[i] === null || value[i] === undefined) {
continue;
}
itemType = introspectType(value[i]);
if (itemType) {
return [itemType];
}
}
return 'array';
}
if (type === 'function') {
return value.constructor.name;
}
var properties = {};
for (var p in value) {
itemType = introspectType(value[p]);
if (itemType) {
properties[p] = itemType;
}
}
if (Object.keys(properties).length === 0) {
return 'object';
}
return properties;
}
ModelBuilder.introspect = introspectType;
return introspectType;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 1 1 1 7 4 4 3 7 7 7 7 7 7 7 7 1 14 46 46 46 46 46 46 46 46 46 46 1 | // Copyright IBM Corp. 2011,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var util = require('util');
/**
*
* @param newClass
* @param baseClass
*/
exports.inherits = function(newClass, baseClass, options) {
util.inherits(newClass, baseClass);
options = options || {
staticProperties: true,
override: false,
};
if (options.staticProperties) {
Object.keys(baseClass).forEach(function(classProp) {
if (classProp !== 'super_' && (!newClass.hasOwnProperty(classProp) ||
options.override)) {
var pd = Object.getOwnPropertyDescriptor(baseClass, classProp);
Object.defineProperty(newClass, classProp, pd);
}
});
}
};
/**
* Mix in the a class into the new class
* @param newClass The target class to receive the mixin
* @param mixinClass The class to be mixed in
* @param options
*/
exports.mixin = function(newClass, mixinClass, options) {
if (Array.isArray(newClass._mixins)) {
Iif (newClass._mixins.indexOf(mixinClass) !== -1) {
return;
}
newClass._mixins.push(mixinClass);
} else {
newClass._mixins = [mixinClass];
}
options = options || {
staticProperties: true,
instanceProperties: true,
override: false,
proxyFunctions: false,
};
Iif (options.staticProperties === undefined) {
options.staticProperties = true;
}
Iif (options.instanceProperties === undefined) {
options.instanceProperties = true;
}
Eif (options.staticProperties) {
mixInto(mixinClass, newClass, options);
}
Eif (options.instanceProperties && mixinClass.prototype) {
mixInto(mixinClass.prototype, newClass.prototype, options);
}
return newClass;
};
function mixInto(sourceScope, targetScope, options) {
Object.keys(sourceScope).forEach(function(propertyName) {
var targetPropertyExists = targetScope.hasOwnProperty(propertyName);
var sourceProperty = Object.getOwnPropertyDescriptor(sourceScope, propertyName);
var targetProperty = targetPropertyExists && Object.getOwnPropertyDescriptor(targetScope, propertyName);
var sourceIsFunc = typeof sourceProperty.value === 'function';
var isFunc = targetPropertyExists && typeof targetProperty.value === 'function';
var isDelegate = isFunc && targetProperty.value._delegate;
var shouldOverride = options.override || !targetPropertyExists || isDelegate;
Iif (propertyName == '_mixins') {
mergeMixins(sourceScope._mixins, targetScope._mixins);
return;
}
Eif (shouldOverride) {
Object.defineProperty(targetScope, propertyName, sourceProperty);
}
});
}
function mergeMixins(source, target) {
// hand-written equivalent of lodash.union()
for (var ix in source) {
var mx = source[ix];
if (target.indexOf(mx) === -1)
target.push(mx);
}
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2012,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('strong-globalize')();
var util = require('util');
var Any = require('./types').Types.Any;
module.exports = List;
function List(items, itemType, parent) {
var list = this;
if (!(list instanceof List)) {
return new List(items, itemType, parent);
}
if (typeof items === 'string') {
try {
items = JSON.parse(items);
} catch (e) {
const err = new Error(g.f('could not create List from JSON string: %j', items));
err.statusCode = 400;
throw err;
}
}
var arr = [];
arr.__proto__ = List.prototype;
items = items || [];
if (!Array.isArray(items)) {
const err = new Error(g.f('Items must be an array: %j', items));
err.statusCode = 400;
throw err;
}
if (!itemType) {
itemType = items[0] && items[0].constructor;
}
if (Array.isArray(itemType)) {
itemType = itemType[0];
}
if (itemType === Array) {
itemType = Any;
}
Object.defineProperty(arr, 'itemType', {
writable: true,
enumerable: false,
value: itemType,
});
if (parent) {
Object.defineProperty(arr, 'parent', {
writable: true,
enumerable: false,
value: parent,
});
}
items.forEach(function(item, i) {
if (itemType && !(item instanceof itemType)) {
arr[i] = itemType(item);
} else {
arr[i] = item;
}
});
return arr;
}
util.inherits(List, Array);
var _push = List.prototype.push;
List.prototype.push = function(obj) {
var item = this.itemType && (obj instanceof this.itemType) ? obj : this.itemType(obj);
_push.call(this, item);
return item;
};
List.prototype.toObject = function(onlySchema, removeHidden, removeProtected) {
var items = [];
this.forEach(function(item) {
if (item && typeof item === 'object' && item.toObject) {
items.push(item.toObject(onlySchema, removeHidden, removeProtected));
} else {
items.push(item);
}
});
return items;
};
List.prototype.toJSON = function() {
return this.toObject(true);
};
List.prototype.toString = function() {
return JSON.stringify(this.toJSON());
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | 1 1 1 1 1 1 2 2 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var debug = require('debug')('loopback:mixin');
var assert = require('assert');
var DefaultModelBaseClass = require('./model.js');
function isModelClass(cls) {
if (!cls) {
return false;
}
return cls.prototype instanceof DefaultModelBaseClass;
}
module.exports = MixinProvider;
function MixinProvider(modelBuilder) {
this.modelBuilder = modelBuilder;
this.mixins = {};
}
/**
* Apply named mixin to the model class
* @param {Model} modelClass
* @param {String} name
* @param {Object} options
*/
MixinProvider.prototype.applyMixin = function applyMixin(modelClass, name, options) {
var fn = this.mixins[name];
if (typeof fn === 'function') {
if (modelClass.dataSource) {
fn(modelClass, options || {});
} else {
modelClass.once('dataSourceAttached', function() {
fn(modelClass, options || {});
});
}
} else {
// Try model name
var model = this.modelBuilder.getModel(name);
if (model) {
debug('Mixin is resolved to a model: %s', name);
modelClass.mixin(model, options);
} else {
var errMsg = 'Model "' + modelClass.modelName + '" uses unknown mixin: ' + name;
debug(errMsg);
throw new Error(errMsg);
}
}
};
/**
* Define a mixin with name
* @param {String} name Name of the mixin
* @param {*) mixin The mixin function or a model
*/
MixinProvider.prototype.define = function defineMixin(name, mixin) {
assert(typeof mixin === 'function', 'The mixin must be a function or model class');
if (this.mixins[name]) {
debug('Duplicate mixin: %s', name);
} else {
debug('Defining mixin: %s', name);
}
if (isModelClass(mixin)) {
this.mixins[name] = function(Model, options) {
Model.mixin(mixin, options);
};
} else if (typeof mixin === 'function') {
this.mixins[name] = mixin;
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 1 1 1 12 12 1 1 1 19 19 19 19 19 19 19 1 1 19 1 1 19 19 19 1 19 19 19 12 12 12 10 2 2 19 19 19 19 3 3 3 19 19 19 304 247 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 1799 1545 19 19 19 19 86 86 86 19 19 19 19 19 19 13 19 19 12 19 1 19 19 13 13 6 19 19 19 12 12 12 12 12 12 27 5 5 12 12 12 12 5 7 7 7 7 7 7 12 12 12 10 12 12 12 12 19 87 87 87 87 87 8 8 87 604 8 596 1 1 1 1 1 1 1 1 1 1 1 1 87 19 19 19 19 87 87 19 19 19 19 19 19 1 1 1 1 1 1 1 171 1 6 6 1 5 6 1 151 151 4 4 4 147 73 73 73 74 66 60 6 8 8 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/*!
* Module dependencies
*/
var g = require('strong-globalize')();
var inflection = require('inflection');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var assert = require('assert');
var deprecated = require('depd')('loopback-datasource-juggler');
var DefaultModelBaseClass = require('./model.js');
var List = require('./list.js');
var ModelDefinition = require('./model-definition.js');
var mergeSettings = require('./utils').mergeSettings;
var MixinProvider = require('./mixins');
// Set up types
require('./types')(ModelBuilder);
var introspect = require('./introspection')(ModelBuilder);
/*!
* Export public API
*/
exports.ModelBuilder = exports.Schema = ModelBuilder;
/*!
* Helpers
*/
var slice = Array.prototype.slice;
/**
* ModelBuilder - A builder to define data models.
*
* @property {Object} definitions Definitions of the models.
* @property {Object} models Model constructors
* @class
*/
function ModelBuilder() {
// create blank models pool
this.models = {};
this.definitions = {};
this.settings = {};
this.mixins = new MixinProvider(this);
this.defaultModelBaseClass = DefaultModelBaseClass;
}
// Inherit from EventEmitter
util.inherits(ModelBuilder, EventEmitter);
// Create a default instance
ModelBuilder.defaultInstance = new ModelBuilder();
function isModelClass(cls) {
Iif (!cls) {
return false;
}
return cls.prototype instanceof DefaultModelBaseClass;
}
/**
* Get a model by name.
*
* @param {String} name The model name
* @param {Boolean} forceCreate Whether the create a stub for the given name if a model doesn't exist.
* @returns {*} The model class
*/
ModelBuilder.prototype.getModel = function(name, forceCreate) {
var model = this.models[name];
if (!model && forceCreate) {
model = this.define(name, {}, {unresolved: true});
}
return model;
};
/**
* Get the model definition by name
* @param {String} name The model name
* @returns {ModelDefinition} The model definition
*/
ModelBuilder.prototype.getModelDefinition = function(name) {
return this.definitions[name];
};
/**
* Define a model class.
* Simple example:
* ```
* var User = modelBuilder.define('User', {
* email: String,
* password: String,
* birthDate: Date,
* activated: Boolean
* });
* ```
* More advanced example:
* ```
* var User = modelBuilder.define('User', {
* email: { type: String, limit: 150, index: true },
* password: { type: String, limit: 50 },
* birthDate: Date,
* registrationDate: {type: Date, default: function () { return new Date }},
* activated: { type: Boolean, default: false }
* });
* ```
*
* @param {String} className Name of class
* @param {Object} properties Hash of class properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
* @param {Object} settings Other configuration of class
* @param {Function} parent Parent model
* @return newly created class
*
*/
ModelBuilder.prototype.define = function defineClass(className, properties, settings, parent) {
var modelBuilder = this;
var args = slice.call(arguments);
var pluralName = (settings && settings.plural) ||
inflection.pluralize(className);
var httpOptions = (settings && settings.http) || {};
var pathName = httpOptions.path || pluralName;
Iif (!className) {
throw new Error(g.f('Class name required'));
}
if (args.length === 1) {
properties = {};
args.push(properties);
}
if (args.length === 2) {
settings = {};
args.push(settings);
}
properties = properties || {};
settings = settings || {};
// Set the strict mode to be false by default
if (settings.strict === undefined || settings.strict === null) {
settings.strict = false;
}
// Set up the base model class
var ModelBaseClass = parent || this.defaultModelBaseClass;
var baseClass = settings.base || settings['super'];
if (baseClass) {
// Normalize base model property
settings.base = baseClass;
delete settings['super'];
if (isModelClass(baseClass)) {
ModelBaseClass = baseClass;
} else {
ModelBaseClass = this.models[baseClass];
assert(ModelBaseClass, 'Base model is not found: ' + baseClass);
}
}
// Make sure base properties are inherited
// See https://github.com/strongloop/loopback-datasource-juggler/issues/293
Iif ((parent && !settings.base) || (!parent && settings.base)) {
return ModelBaseClass.extend(className, properties, settings);
}
// Check if there is a unresolved model with the same name
var ModelClass = this.models[className];
// Create the ModelClass if it doesn't exist or it's resolved (override)
// TODO: [rfeng] We need to decide what names to use for built-in models such as User.
Eif (!ModelClass || !ModelClass.settings.unresolved) {
// every class can receive hash of data as optional param
ModelClass = function ModelConstructor(data, options) {
Iif (!(this instanceof ModelConstructor)) {
return new ModelConstructor(data, options);
}
Iif (ModelClass.settings.unresolved) {
throw new Error(g.f('Model %s is not defined.', ModelClass.modelName));
}
ModelBaseClass.apply(this, arguments);
};
// mix in EventEmitter (don't inherit from)
var events = new EventEmitter();
// The model can have more than 10 listeners for lazy relationship setup
// See https://github.com/strongloop/loopback/issues/404
events.setMaxListeners(32);
for (var f in EventEmitter.prototype) {
if (typeof EventEmitter.prototype[f] === 'function') {
ModelClass[f] = EventEmitter.prototype[f].bind(events);
}
}
hiddenProperty(ModelClass, 'modelName', className);
}
util.inherits(ModelClass, ModelBaseClass);
// store class in model pool
this.models[className] = ModelClass;
// Return the unresolved model
Iif (settings.unresolved) {
ModelClass.settings = {unresolved: true};
return ModelClass;
}
// Add metadata to the ModelClass
hiddenProperty(ModelClass, 'modelBuilder', modelBuilder);
hiddenProperty(ModelClass, 'dataSource', null); // Keep for back-compatibility
hiddenProperty(ModelClass, 'pluralModelName', pluralName);
hiddenProperty(ModelClass, 'relations', {});
Eif (pathName[0] !== '/') {
// Support both flavors path: 'x' and path: '/x'
pathName = '/' + pathName;
}
hiddenProperty(ModelClass, 'http', {path: pathName});
hiddenProperty(ModelClass, 'base', ModelBaseClass);
hiddenProperty(ModelClass, '_observers', {});
hiddenProperty(ModelClass, '_warned', {});
// inherit ModelBaseClass static methods
for (var i in ModelBaseClass) {
// We need to skip properties that are already in the subclass, for example, the event emitter methods
if (i !== '_mixins' && !(i in ModelClass)) {
ModelClass[i] = ModelBaseClass[i];
}
}
// Load and inject the model classes
Iif (settings.models) {
Object.keys(settings.models).forEach(function(m) {
var model = settings.models[m];
ModelClass[m] = typeof model === 'string' ? modelBuilder.getModel(model, true) : model;
});
}
ModelClass.getter = {};
ModelClass.setter = {};
for (var p in properties) {
// Remove properties that reverted by the subclass
Iif (properties[p] === null || properties[p] === false) {
// Hide the base property
delete properties[p];
}
// Throw error for properties with unsupported names
Iif (/\./.test(p)) {
throw new Error(g.f('Property names containing dot(s) are not supported. ' +
'Model: %s, property: %s', className, p));
}
// Warn if property name is 'constructor'
Iif (p === 'constructor') {
deprecated(g.f('Property name should not be "{{constructor}}" in Model: %s', className));
}
}
var modelDefinition = new ModelDefinition(this, className, properties, settings);
this.definitions[className] = modelDefinition;
// expose properties on the ModelClass
ModelClass.definition = modelDefinition;
// keep a pointer to settings as models can use it for configuration
ModelClass.settings = modelDefinition.settings;
var idInjection = settings.idInjection;
if (idInjection !== false) {
// Default to true if undefined
idInjection = true;
}
var idNames = modelDefinition.idNames();
if (idNames.length > 0) {
// id already exists
idInjection = false;
}
// Add the id property
if (idInjection) {
// Set up the id property
ModelClass.definition.defineProperty('id', {type: Number, id: 1, generated: true});
}
idNames = modelDefinition.idNames(); // Reload it after rebuild
// Create a virtual property 'id'
if (idNames.length === 1) {
var idProp = idNames[0];
Iif (idProp !== 'id') {
Object.defineProperty(ModelClass.prototype, 'id', {
get: function() {
var idProp = ModelClass.definition.idNames()[0];
return this.__data[idProp];
},
configurable: true,
enumerable: false,
});
}
} else {
// Now the id property is an object that consists of multiple keys
Object.defineProperty(ModelClass.prototype, 'id', {
get: function() {
var compositeId = {};
var idNames = ModelClass.definition.idNames();
for (var i = 0, p; i < idNames.length; i++) {
p = idNames[i];
compositeId[p] = this.__data[p];
}
return compositeId;
},
configurable: true,
enumerable: false,
});
}
// A function to loop through the properties
ModelClass.forEachProperty = function(cb) {
var props = ModelClass.definition.properties;
var keys = Object.keys(props);
for (var i = 0, n = keys.length; i < n; i++) {
cb(keys[i], props[keys[i]]);
}
};
// A function to attach the model class to a data source
ModelClass.attachTo = function(dataSource) {
dataSource.attach(this);
};
/** Extend the model with the specified model, properties, and other settings.
* For example, to extend an existing model, for example, a built-in model:
*
* ```js
* var Customer = User.extend('customer', {
* accountId: String,
* vip: Boolean
* });
* ```
*
* To extend the base model, essentially creating a new model:
* ```js
* var user = loopback.Model.extend('user', properties, options);
* ```
*
* @param {String} className Name of the new model being defined.
* @options {Object} properties Properties to define for the model, added to properties of model being extended.
* @options {Object} settings Model settings, such as relations and acls.
*
*/
ModelClass.extend = function(className, subclassProperties, subclassSettings) {
var properties = ModelClass.definition.properties;
var settings = ModelClass.definition.settings;
subclassProperties = subclassProperties || {};
subclassSettings = subclassSettings || {};
// Check if subclass redefines the ids
var idFound = false;
for (var k in subclassProperties) {
if (subclassProperties[k] && subclassProperties[k].id) {
idFound = true;
break;
}
}
// Merging the properties
var keys = Object.keys(properties);
for (var i = 0, n = keys.length; i < n; i++) {
var key = keys[i];
if (idFound && properties[key].id) {
// don't inherit id properties
continue;
}
Eif (subclassProperties[key] === undefined) {
var baseProp = properties[key];
var basePropCopy = baseProp;
Eif (baseProp && typeof baseProp === 'object') {
// Deep clone the base prop
basePropCopy = mergeSettings(null, baseProp);
}
subclassProperties[key] = basePropCopy;
}
}
// Merge the settings
var originalSubclassSettings = subclassSettings;
subclassSettings = mergeSettings(settings, subclassSettings);
// Ensure 'base' is not inherited. Note we don't have to delete 'super'
// as that is removed from settings by modelBuilder.define and thus
// it is never inherited
if (!originalSubclassSettings.base) {
subclassSettings.base = ModelClass;
}
// Define the subclass
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
// Calling the setup function
Eif (typeof subClass.setup === 'function') {
subClass.setup.call(subClass);
}
return subClass;
};
/**
* Register a property for the model class
* @param {String} propertyName Name of the property.
*/
ModelClass.registerProperty = function(propertyName) {
var properties = modelDefinition.build();
var prop = properties[propertyName];
var DataType = prop.type;
Iif (!DataType) {
throw new Error(g.f('Invalid type for property %s', propertyName));
}
if (prop.required) {
var requiredOptions = typeof prop.required === 'object' ? prop.required : undefined;
ModelClass.validatesPresenceOf(propertyName, requiredOptions);
}
Object.defineProperty(ModelClass.prototype, propertyName, {
get: function() {
if (ModelClass.getter[propertyName]) {
return ModelClass.getter[propertyName].call(this); // Try getter first
} else {
return this.__data && this.__data[propertyName]; // Try __data
}
},
set: function(value) {
var DataType = ModelClass.definition.properties[propertyName].type;
Iif (Array.isArray(DataType) || DataType === Array) {
DataType = List;
} else Iif (DataType === Date) {
DataType = DateType;
} else Iif (DataType === Boolean) {
DataType = BooleanType;
} else Iif (typeof DataType === 'string') {
DataType = modelBuilder.resolveType(DataType);
}
var persistUndefinedAsNull = ModelClass.definition.settings.persistUndefinedAsNull;
Iif (value === undefined && persistUndefinedAsNull) {
value = null;
}
Iif (ModelClass.setter[propertyName]) {
ModelClass.setter[propertyName].call(this, value); // Try setter first
} else {
this.__data = this.__data || {};
Iif (value === null || value === undefined) {
this.__data[propertyName] = value;
} else {
Iif (DataType === List) {
this.__data[propertyName] = DataType(value, properties[propertyName].type, this.__data);
} else {
// Assume the type constructor handles Constructor() call
// If not, we should call new DataType(value).valueOf();
this.__data[propertyName] = (value instanceof DataType) ? value : DataType(value);
}
}
}
},
configurable: true,
enumerable: true,
});
// FIXME: [rfeng] Do we need to keep the raw data?
// Use $ as the prefix to avoid conflicts with properties such as _id
Object.defineProperty(ModelClass.prototype, '$' + propertyName, {
get: function() {
return this.__data && this.__data[propertyName];
},
set: function(value) {
if (!this.__data) {
this.__data = {};
}
this.__data[propertyName] = value;
},
configurable: true,
enumerable: false,
});
};
var props = ModelClass.definition.properties;
var keys = Object.keys(props);
var size = keys.length;
for (i = 0; i < size; i++) {
var propertyName = keys[i];
ModelClass.registerProperty(propertyName);
}
var mixinSettings = settings.mixins || {};
keys = Object.keys(mixinSettings);
size = keys.length;
for (i = 0; i < size; i++) {
var name = keys[i];
var mixin = mixinSettings[name];
if (mixin === true) {
mixin = {};
}
if (Array.isArray(mixin)) {
mixin.forEach(function(m) {
if (m === true) m = {};
if (typeof m === 'object') {
modelBuilder.mixins.applyMixin(ModelClass, name, m);
}
});
} else if (typeof mixin === 'object') {
modelBuilder.mixins.applyMixin(ModelClass, name, mixin);
}
}
ModelClass.emit('defined', ModelClass);
return ModelClass;
};
// DataType for Date
function DateType(arg) {
var d = new Date(arg);
if (isNaN(d.getTime())) {
throw new Error(g.f('Invalid date: %s', arg));
}
return d;
}
// Relax the Boolean coercision
function BooleanType(arg) {
if (typeof arg === 'string') {
switch (arg) {
case 'true':
case '1':
return true;
case 'false':
case '0':
return false;
}
}
if (arg == null) {
return null;
}
return Boolean(arg);
}
/**
* Define single property named `propertyName` on `model`
*
* @param {String} model Name of model
* @param {String} propertyName Name of property
* @param {Object} propertyDefinition Property settings
*/
ModelBuilder.prototype.defineProperty = function(model, propertyName, propertyDefinition) {
this.definitions[model].defineProperty(propertyName, propertyDefinition);
this.models[model].registerProperty(propertyName);
};
/**
* Define a new value type that can be used in model schemas as a property type.
* @param {function()} type Type constructor.
* @param {string[]=} aliases Optional list of alternative names for this type.
*/
ModelBuilder.prototype.defineValueType = function(type, aliases) {
ModelBuilder.registerType(type, aliases);
};
/**
* Extend existing model with specified properties
*
* Example:
* Instead of extending a model with attributes like this (for example):
*
* ```js
* db.defineProperty('Content', 'competitionType',
* { type: String });
* db.defineProperty('Content', 'expiryDate',
* { type: Date, index: true });
* db.defineProperty('Content', 'isExpired',
* { type: Boolean, index: true });
*```
* This method enables you to extend a model as follows (for example):
* ```js
* db.extendModel('Content', {
* competitionType: String,
* expiryDate: { type: Date, index: true },
* isExpired: { type: Boolean, index: true }
* });
*```
*
* @param {String} model Name of model
* @options {Object} properties JSON object specifying properties. Each property is a key whos value is
* either the [type](http://docs.strongloop.com/display/LB/LoopBack+types) or `propertyName: {options}`
* where the options are described below.
* @property {String} type Datatype of property: Must be an [LDL type](http://docs.strongloop.com/display/LB/LoopBack+types).
* @property {Boolean} index True if the property is an index; false otherwise.
*/
ModelBuilder.prototype.extendModel = function(model, props) {
var t = this;
var keys = Object.keys(props);
for (var i = 0; i < keys.length; i++) {
var definition = props[keys[i]];
t.defineProperty(model, keys[i], definition);
}
};
ModelBuilder.prototype.copyModel = function copyModel(Master) {
var modelBuilder = this;
var className = Master.modelName;
var md = Master.modelBuilder.definitions[className];
var Slave = function SlaveModel() {
Master.apply(this, [].slice.call(arguments));
};
util.inherits(Slave, Master);
Slave.__proto__ = Master;
hiddenProperty(Slave, 'modelBuilder', modelBuilder);
hiddenProperty(Slave, 'modelName', className);
hiddenProperty(Slave, 'relations', Master.relations);
if (!(className in modelBuilder.models)) {
// store class in model pool
modelBuilder.models[className] = Slave;
modelBuilder.definitions[className] = {
properties: md.properties,
settings: md.settings,
};
}
return Slave;
};
/*!
* Define hidden property
*/
function hiddenProperty(where, property, value) {
Object.defineProperty(where, property, {
writable: true,
enumerable: false,
configurable: true,
value: value,
});
}
/**
* Get the schema name
*/
ModelBuilder.prototype.getSchemaName = function(name) {
Iif (name) {
return name;
}
if (typeof this._nameCount !== 'number') {
this._nameCount = 0;
} else {
this._nameCount++;
}
return 'AnonymousModel_' + this._nameCount;
};
/**
* Resolve the type string to be a function, for example, 'String' to String.
* Returns {Function} if the type is resolved
* @param {String} type The type string, such as 'number', 'Number', 'boolean', or 'String'. It's case insensitive
*/
ModelBuilder.prototype.resolveType = function(type) {
Iif (!type) {
return type;
}
if (Array.isArray(type) && type.length > 0) {
// For array types, the first item should be the type string
var itemType = this.resolveType(type[0]);
Eif (typeof itemType === 'function') {
return [itemType];
} else {
return itemType; // Not resolved, return the type string
}
}
if (typeof type === 'string') {
var schemaType = ModelBuilder.schemaTypes[type.toLowerCase()] || this.models[type];
Eif (schemaType) {
return schemaType;
} else {
// The type cannot be resolved, let's create a place holder
type = this.define(type, {}, {unresolved: true});
return type;
}
} else if (type.constructor.name === 'Object') {
// We also support the syntax {type: 'string', ...}
if (type.type) {
return this.resolveType(type.type);
} else {
return this.define(this.getSchemaName(null),
type, {
anonymous: true,
idInjection: false,
strict: this.settings.strictEmbeddedModels || false,
});
}
} else Eif ('function' === typeof type) {
return type;
}
return type;
};
/**
* Build models from schema definitions
*
* `schemas` can be one of the following:
*
* 1. An array of named schema definition JSON objects
* 2. A schema definition JSON object
* 3. A list of property definitions (anonymous)
*
* @param {*} schemas The schemas
* @returns {Object} A map of model constructors keyed by model name
*/
ModelBuilder.prototype.buildModels = function(schemas, createModel) {
var models = {};
// Normalize the schemas to be an array of the schema objects {name: <name>, properties: {}, options: {}}
if (!Array.isArray(schemas)) {
if (schemas.properties && schemas.name) {
// Only one item
schemas = [schemas];
} else {
// Anonymous schema
schemas = [
{
name: this.getSchemaName(),
properties: schemas,
options: {anonymous: true},
},
];
}
}
var relations = [];
for (var s = 0, n = schemas.length; s < n; s++) {
var name = this.getSchemaName(schemas[s].name);
schemas[s].name = name;
var model;
if (typeof createModel === 'function') {
model = createModel(schemas[s].name, schemas[s].properties, schemas[s].options);
} else {
model = this.define(schemas[s].name, schemas[s].properties, schemas[s].options);
}
models[name] = model;
relations = relations.concat(model.definition.relations);
}
// Connect the models based on the relations
for (var i = 0; i < relations.length; i++) {
var relation = relations[i];
var sourceModel = models[relation.source];
var targetModel = models[relation.target];
if (sourceModel && targetModel) {
if (typeof sourceModel[relation.type] === 'function') {
sourceModel[relation.type](targetModel, {as: relation.as});
}
}
}
return models;
};
/**
* Introspect the JSON document to build a corresponding model.
* @param {String} name The model name
* @param {Object} json The JSON object
* @param {Object} options The options
* @returns {}
*/
ModelBuilder.prototype.buildModelFromInstance = function(name, json, options) {
// Introspect the JSON document to generate a schema
var schema = introspect(json);
// Create a model for the generated schema
return this.define(name, schema, options);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | 1 1 1 1 1 1 1 1 19 19 19 19 19 19 19 19 19 19 19 1 1 1 1 1 1 1 38 18 20 20 20 20 87 87 74 13 5 13 20 20 20 1 1 1 38 38 25 38 1 1 127 1 1 1 1 127 107 20 20 87 87 87 87 87 64 127 67 87 20 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var traverse = require('traverse');
var ModelBaseClass = require('./model');
var ModelBuilder = require('./model-builder');
/**
* Model definition
*/
module.exports = ModelDefinition;
/**
* Constructor for ModelDefinition
* @param {ModelBuilder} modelBuilder A model builder instance
* @param {String|Object} name The model name or the schema object
* @param {Object} properties The model properties, optional
* @param {Object} settings The model settings, optional
* @returns {ModelDefinition}
* @constructor
*
*/
function ModelDefinition(modelBuilder, name, properties, settings) {
Iif (!(this instanceof ModelDefinition)) {
// Allow to call ModelDefinition without new
return new ModelDefinition(modelBuilder, name, properties, settings);
}
this.modelBuilder = modelBuilder || ModelBuilder.defaultInstance;
assert(name, 'name is missing');
Iif (arguments.length === 2 && typeof name === 'object') {
var schema = name;
this.name = schema.name;
this.rawProperties = schema.properties || {}; // Keep the raw property definitions
this.settings = schema.settings || {};
} else {
assert(typeof name === 'string', 'name must be a string');
this.name = name;
this.rawProperties = properties || {}; // Keep the raw property definitions
this.settings = settings || {};
}
this.relations = [];
this.properties = null;
this.build();
}
util.inherits(ModelDefinition, EventEmitter);
// Set up types
require('./types')(ModelDefinition);
/**
* Return table name for specified `modelName`
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
*/
ModelDefinition.prototype.tableName = function(connectorType) {
var settings = this.settings;
if (settings[connectorType]) {
return settings[connectorType].table || settings[connectorType].tableName || this.name;
} else {
return this.name;
}
};
/**
* Return column name for specified modelName and propertyName
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
* @param propertyName The property name
* @returns {String} columnName
*/
ModelDefinition.prototype.columnName = function(connectorType, propertyName) {
if (!propertyName) {
return propertyName;
}
this.build();
var property = this.properties[propertyName];
if (property && property[connectorType]) {
return property[connectorType].column || property[connectorType].columnName || propertyName;
} else {
return propertyName;
}
};
/**
* Return column metadata for specified modelName and propertyName
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
* @param propertyName The property name
* @returns {Object} column metadata
*/
ModelDefinition.prototype.columnMetadata = function(connectorType, propertyName) {
if (!propertyName) {
return propertyName;
}
this.build();
var property = this.properties[propertyName];
if (property && property[connectorType]) {
return property[connectorType];
} else {
return null;
}
};
/**
* Return column names for specified modelName
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
* @returns {String[]} column names
*/
ModelDefinition.prototype.columnNames = function(connectorType) {
this.build();
var props = this.properties;
var cols = [];
for (var p in props) {
if (props[p][connectorType]) {
cols.push(props[p][connectorType].column || props[p][connectorType].columnName || p);
} else {
cols.push(p);
}
}
return cols;
};
/**
* Find the ID properties sorted by the index
* @returns {Object[]} property name/index for IDs
*/
ModelDefinition.prototype.ids = function() {
if (this._ids) {
return this._ids;
}
var ids = [];
this.build();
var props = this.properties;
for (var key in props) {
var id = props[key].id;
if (!id) {
continue;
}
if (typeof id !== 'number') {
id = 1;
}
ids.push({name: key, id: id, property: props[key]});
}
ids.sort(function(a, b) {
return a.id - b.id;
});
this._ids = ids;
return ids;
};
/**
* Find the ID column name
* @param {String} modelName The model name
* @returns {String} columnName for ID
*/
ModelDefinition.prototype.idColumnName = function(connectorType) {
return this.columnName(connectorType, this.idName());
};
/**
* Find the ID property name
* @returns {String} property name for ID
*/
ModelDefinition.prototype.idName = function() {
var id = this.ids()[0];
if (this.properties.id && this.properties.id.id) {
return 'id';
} else {
return id && id.name;
}
};
/**
* Find the ID property names sorted by the index
* @returns {String[]} property names for IDs
*/
ModelDefinition.prototype.idNames = function() {
var ids = this.ids();
var names = ids.map(function(id) {
return id.name;
});
return names;
};
/**
*
* @returns {{}}
*/
ModelDefinition.prototype.indexes = function() {
this.build();
var indexes = {};
if (this.settings.indexes) {
for (var i in this.settings.indexes) {
indexes[i] = this.settings.indexes[i];
}
}
for (var p in this.properties) {
if (this.properties[p].index) {
indexes[p + '_index'] = this.properties[p].index;
}
}
return indexes;
};
/**
* Build a model definition
* @param {Boolean} force Forcing rebuild
*/
ModelDefinition.prototype.build = function(forceRebuild) {
if (forceRebuild) {
this.properties = null;
this.relations = [];
this._ids = null;
this.json = null;
}
if (this.properties) {
return this.properties;
}
this.properties = {};
for (var p in this.rawProperties) {
var prop = this.rawProperties[p];
var type = this.modelBuilder.resolveType(prop);
Iif (typeof type === 'string') {
this.relations.push({
source: this.name,
target: type,
type: Array.isArray(prop) ? 'hasMany' : 'belongsTo',
as: p,
});
} else {
var typeDef = {
type: type,
};
if (typeof prop === 'object' && prop !== null) {
for (var a in prop) {
// Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class
if (a !== 'type') {
typeDef[a] = prop[a];
}
}
}
this.properties[p] = typeDef;
}
}
return this.properties;
};
/**
* Define a property
* @param {String} propertyName The property name
* @param {Object} propertyDefinition The property definition
*/
ModelDefinition.prototype.defineProperty = function(propertyName, propertyDefinition) {
this.rawProperties[propertyName] = propertyDefinition;
this.build(true);
};
function isModelClass(cls) {
if (!cls) {
return false;
}
return cls.prototype instanceof ModelBaseClass;
}
ModelDefinition.prototype.toJSON = function(forceRebuild) {
if (forceRebuild) {
this.json = null;
}
if (this.json) {
return this.json;
}
var json = {
name: this.name,
properties: {},
settings: this.settings,
};
this.build(forceRebuild);
var mapper = function(val) {
if (val === undefined || val === null) {
return val;
}
if ('function' === typeof val.toJSON) {
// The value has its own toJSON() object
return val.toJSON();
}
if ('function' === typeof val) {
if (isModelClass(val)) {
if (val.settings && val.settings.anonymous) {
return val.definition && val.definition.toJSON().properties;
} else {
return val.modelName;
}
}
return val.name;
} else {
return val;
}
};
for (var p in this.properties) {
json.properties[p] = traverse(this.properties[p]).map(mapper);
}
this.json = json;
return json;
};
ModelDefinition.prototype.hasPK = function() {
return this.ids().length > 0;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3 3 3 2 2 1 1 1 3 1 1 1 1 1 1 3 3 1 1 1 1 1 1 1 1 1 1 1 1 3 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// Turning on strict for this file breaks lots of test cases;
// disabling strict for this file
/* eslint-disable strict */
/*!
* Module exports class Model
*/
module.exports = ModelBaseClass;
/*!
* Module dependencies
*/
var g = require('strong-globalize')();
var util = require('util');
var jutil = require('./jutil');
var List = require('./list');
var Hookable = require('./hooks');
var validations = require('./validations');
var _extend = util._extend;
var utils = require('./utils');
var fieldsToArray = utils.fieldsToArray;
var uuid = require('uuid');
var shortid = require('shortid');
// Set up an object for quick lookup
var BASE_TYPES = {
'String': true,
'Boolean': true,
'Number': true,
'Date': true,
'Text': true,
'ObjectID': true,
};
/**
* Model class: base class for all persistent objects.
*
* `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods
*
* @class
* @param {Object} data Initial object data
*/
function ModelBaseClass(data, options) {
options = options || {};
Eif (!('applySetters' in options)) {
// Default to true
options.applySetters = true;
}
Eif (!('applyDefaultValues' in options)) {
options.applyDefaultValues = true;
}
this._initProperties(data, options);
}
/**
* Initialize the model instance with a list of properties
* @param {Object} data The data object
* @param {Object} options An object to control the instantiation
* @property {Boolean} applySetters Controls if the setters will be applied
* @property {Boolean} applyDefaultValues Default attributes and values will be applied
* @property {Boolean} strict Set the instance level strict mode
* @property {Boolean} persisted Whether the instance has been persisted
* @private
*/
ModelBaseClass.prototype._initProperties = function(data, options) {
var self = this;
var ctor = this.constructor;
// issue#1261
Iif (typeof data !== 'undefined' && data.constructor &&
typeof (data.constructor) !== 'function') {
throw new Error(g.f('Property name "{{constructor}}" is not allowed in %s data', ctor.modelName));
}
Iif (data instanceof ctor) {
// Convert the data to be plain object to avoid pollutions
data = data.toObject(false);
}
var properties = _extend({}, ctor.definition.properties);
data = data || {};
Iif (typeof ctor.applyProperties === 'function') {
ctor.applyProperties(data);
}
options = options || {};
var applySetters = options.applySetters;
var applyDefaultValues = options.applyDefaultValues;
var strict = options.strict;
Eif (strict === undefined) {
strict = ctor.definition.settings.strict;
} else if (strict === 'throw') {
g.warn('Warning: Model %s, {{strict mode: `throw`}} has been removed, ' +
'please use {{`strict: true`}} instead, which returns' +
'{{`Validation Error`}} for unknown properties,', ctor.modelName);
}
var persistUndefinedAsNull = ctor.definition.settings.persistUndefinedAsNull;
Iif (ctor.hideInternalProperties) {
// Object.defineProperty() is expensive. We only try to make the internal
// properties hidden (non-enumerable) if the model class has the
// `hideInternalProperties` set to true
Object.defineProperties(this, {
__cachedRelations: {
writable: true,
enumerable: false,
configurable: true,
value: {},
},
__data: {
writable: true,
enumerable: false,
configurable: true,
value: {},
},
// Instance level data source
__dataSource: {
writable: true,
enumerable: false,
configurable: true,
value: options.dataSource,
},
// Instance level strict mode
__strict: {
writable: true,
enumerable: false,
configurable: true,
value: strict,
},
__persisted: {
writable: true,
enumerable: false,
configurable: true,
value: false,
},
});
if (strict) {
Object.defineProperty(this, '__unknownProperties', {
writable: true,
enumerable: false,
configrable: true,
value: [],
});
}
} else {
this.__cachedRelations = {};
this.__data = {};
this.__dataSource = options.dataSource;
this.__strict = strict;
this.__persisted = false;
Iif (strict) {
this.__unknownProperties = [];
}
}
Iif (options.persisted !== undefined) {
this.__persisted = options.persisted === true;
}
Iif (data.__cachedRelations) {
this.__cachedRelations = data.__cachedRelations;
}
var keys = Object.keys(data);
Iif (Array.isArray(options.fields)) {
keys = keys.filter(function(k) {
return (options.fields.indexOf(k) != -1);
});
}
var size = keys.length;
var p, propVal;
for (var k = 0; k < size; k++) {
p = keys[k];
propVal = data[p];
Iif (typeof propVal === 'function') {
continue;
}
Iif (propVal === undefined && persistUndefinedAsNull) {
propVal = null;
}
Eif (properties[p]) {
// Managed property
Eif (applySetters || properties[p].id) {
self[p] = propVal;
} else {
self.__data[p] = propVal;
}
} else if (ctor.relations[p]) {
var relationType = ctor.relations[p].type;
var modelTo;
if (!properties[p]) {
modelTo = ctor.relations[p].modelTo || ModelBaseClass;
var multiple = ctor.relations[p].multiple;
var typeName = multiple ? 'Array' : modelTo.modelName;
var propType = multiple ? [modelTo] : modelTo;
properties[p] = {name: typeName, type: propType};
/* Issue #1252
this.setStrict(false);
*/
}
// Relation
if (relationType === 'belongsTo' && propVal != null) {
// If the related model is populated
self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];
if (ctor.relations[p].options.embedsProperties) {
var fields = fieldsToArray(ctor.relations[p].properties,
modelTo.definition.properties, modelTo.settings.strict);
if (!~fields.indexOf(ctor.relations[p].keyTo)) {
fields.push(ctor.relations[p].keyTo);
}
self.__data[p] = new modelTo(propVal, {
fields: fields,
applySetters: false,
persisted: options.persisted,
});
}
}
self.__cachedRelations[p] = propVal;
} else {
// Un-managed property
if (strict === false || self.__cachedRelations[p]) {
self[p] = self.__data[p] =
(propVal !== undefined) ? propVal : self.__cachedRelations[p];
// Throw error for properties with unsupported names
if (/\./.test(p)) {
throw new Error(g.f(
'Property names containing dot(s) are not supported. ' +
'Model: %s, dynamic property: %s',
this.constructor.modelName, p
));
}
} else {
if (strict !== 'filter') {
this.__unknownProperties.push(p);
}
}
}
}
keys = Object.keys(properties);
Iif (Array.isArray(options.fields)) {
keys = keys.filter(function(k) {
return (options.fields.indexOf(k) != -1);
});
}
size = keys.length;
for (k = 0; k < size; k++) {
p = keys[k];
propVal = self.__data[p];
var type = properties[p].type;
// Set default values
if (applyDefaultValues && propVal === undefined) {
var def = properties[p]['default'];
if (def !== undefined) {
Iif (typeof def === 'function') {
if (def === Date) {
// FIXME: We should coerce the value in general
// This is a work around to {default: Date}
// Date() will return a string instead of Date
def = new Date();
} else {
def = def();
}
} else Iif (type.name === 'Date' && def === '$now') {
def = new Date();
}
// FIXME: We should coerce the value
// will implement it after we refactor the PropertyDefinition
self.__data[p] = propVal = def;
}
}
// Set default value using a named function
if (applyDefaultValues && propVal === undefined) {
var defn = properties[p].defaultFn;
switch (defn) {
case undefined:
break;
case 'guid':
case 'uuid':
// Generate a v1 (time-based) id
propVal = uuid.v1();
break;
case 'uuidv4':
// Generate a RFC4122 v4 UUID
propVal = uuid.v4();
break;
case 'now':
propVal = new Date();
break;
case 'shortid':
propVal = shortid.generate();
break;
default:
// TODO Support user-provided functions via a registry of functions
g.warn('Unknown default value provider %s', defn);
}
// FIXME: We should coerce the value
// will implement it after we refactor the PropertyDefinition
Eif (propVal !== undefined)
self.__data[p] = propVal;
}
Iif (propVal === undefined && persistUndefinedAsNull) {
self.__data[p] = propVal = null;
}
// Handle complex types (JSON/Object)
Iif (!BASE_TYPES[type.name]) {
if (typeof self.__data[p] !== 'object' && self.__data[p]) {
try {
self.__data[p] = JSON.parse(self.__data[p] + '');
} catch (e) {
self.__data[p] = String(self.__data[p]);
}
}
if (type.prototype instanceof ModelBaseClass) {
if (!(self.__data[p] instanceof type) &&
typeof self.__data[p] === 'object' &&
self.__data[p] !== null) {
self.__data[p] = new type(self.__data[p]);
}
} else if (type.name === 'Array' || Array.isArray(type)) {
if (!(self.__data[p] instanceof List) &&
self.__data[p] !== undefined &&
self.__data[p] !== null) {
self.__data[p] = List(self.__data[p], type, self);
}
}
}
}
this.trigger('initialize');
};
/**
* Define a property on the model.
* @param {String} prop Property name
* @param {Object} params Various property configuration
*/
ModelBaseClass.defineProperty = function(prop, params) {
if (this.dataSource) {
this.dataSource.defineProperty(this.modelName, prop, params);
} else {
this.modelBuilder.defineProperty(this.modelName, prop, params);
}
};
ModelBaseClass.getPropertyType = function(propName) {
var prop = this.definition.properties[propName];
if (!prop) {
// The property is not part of the definition
return null;
}
if (!prop.type) {
throw new Error(g.f('Type not defined for property %s.%s', this.modelName, propName));
// return null;
}
return prop.type.name;
};
ModelBaseClass.prototype.getPropertyType = function(propName) {
return this.constructor.getPropertyType(propName);
};
/**
* Return string representation of class
* This overrides the default `toString()` method
*/
ModelBaseClass.toString = function() {
return '[Model ' + this.modelName + ']';
};
/**
* Convert model instance to a plain JSON object.
* Returns a canonical object representation (no getters and setters).
*
* @param {Boolean} onlySchema Restrict properties to dataSource only. Default is false. If true, the function returns only properties defined in the schema; Otherwise it returns all enumerable properties.
*/
ModelBaseClass.prototype.toObject = function(onlySchema, removeHidden, removeProtected) {
if (onlySchema === undefined) {
onlySchema = true;
}
var data = {};
var self = this;
var Model = this.constructor;
// if it is already an Object
if (Model === Object) {
return self;
}
var strict = this.__strict;
var schemaLess = (strict === false) || !onlySchema;
var persistUndefinedAsNull = Model.definition.settings.persistUndefinedAsNull;
var props = Model.definition.properties;
var keys = Object.keys(props);
var propertyName, val;
for (var i = 0; i < keys.length; i++) {
propertyName = keys[i];
val = self[propertyName];
// Exclude functions
if (typeof val === 'function') {
continue;
}
// Exclude hidden properties
if (removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
if (removeProtected && Model.isProtectedProperty(propertyName)) {
continue;
}
if (val instanceof List) {
data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
} else {
if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
} else {
if (val === undefined && persistUndefinedAsNull) {
val = null;
}
data[propertyName] = val;
}
}
}
if (schemaLess) {
// Find its own properties which can be set via myModel.myProperty = 'myValue'.
// If the property is not declared in the model definition, no setter will be
// triggered to add it to __data
keys = Object.keys(self);
var size = keys.length;
for (i = 0; i < size; i++) {
propertyName = keys[i];
if (props[propertyName]) {
continue;
}
if (propertyName.indexOf('__') === 0) {
continue;
}
if (removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
if (removeProtected && Model.isProtectedProperty(propertyName)) {
continue;
}
if (data[propertyName] !== undefined) {
continue;
}
val = self[propertyName];
if (val !== undefined) {
if (typeof val === 'function') {
continue;
}
if (val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
} else {
data[propertyName] = val;
}
} else if (persistUndefinedAsNull) {
data[propertyName] = null;
}
}
// Now continue to check __data
keys = Object.keys(self.__data);
size = keys.length;
for (i = 0; i < size; i++) {
propertyName = keys[i];
if (propertyName.indexOf('__') === 0) {
continue;
}
if (data[propertyName] === undefined) {
if (removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
if (removeProtected && Model.isProtectedProperty(propertyName)) {
continue;
}
var ownVal = self[propertyName];
// The ownVal can be a relation function
val = (ownVal !== undefined && (typeof ownVal !== 'function')) ? ownVal : self.__data[propertyName];
if (typeof val === 'function') {
continue;
}
if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
} else if (val === undefined && persistUndefinedAsNull) {
data[propertyName] = null;
} else {
data[propertyName] = val;
}
}
}
}
return data;
};
ModelBaseClass.isProtectedProperty = function(propertyName) {
var Model = this;
var settings = Model.definition && Model.definition.settings;
var protectedProperties = settings && (settings.protectedProperties || settings.protected);
if (Array.isArray(protectedProperties)) {
// Cache the protected properties as an object for quick lookup
settings.protectedProperties = {};
for (var i = 0; i < protectedProperties.length; i++) {
settings.protectedProperties[protectedProperties[i]] = true;
}
protectedProperties = settings.protectedProperties;
}
if (protectedProperties) {
return protectedProperties[propertyName];
} else {
return false;
}
};
ModelBaseClass.isHiddenProperty = function(propertyName) {
var Model = this;
var settings = Model.definition && Model.definition.settings;
var hiddenProperties = settings && (settings.hiddenProperties || settings.hidden);
if (Array.isArray(hiddenProperties)) {
// Cache the hidden properties as an object for quick lookup
settings.hiddenProperties = {};
for (var i = 0; i < hiddenProperties.length; i++) {
settings.hiddenProperties[hiddenProperties[i]] = true;
}
hiddenProperties = settings.hiddenProperties;
}
if (hiddenProperties) {
return hiddenProperties[propertyName];
} else {
return false;
}
};
ModelBaseClass.prototype.toJSON = function() {
return this.toObject(false, true, false);
};
ModelBaseClass.prototype.fromObject = function(obj) {
for (var key in obj) {
this[key] = obj[key];
}
};
/**
* Reset dirty attributes.
* This method does not perform any database operations; it just resets the object to its
* initial state.
*/
ModelBaseClass.prototype.reset = function() {
var obj = this;
for (var k in obj) {
if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
delete obj[k];
}
}
};
// Node v0.11+ allows custom inspect functions to return an object
// instead of string. That way options like `showHidden` and `colors`
// can be preserved.
var versionParts = process.versions && process.versions.node ?
process.versions.node.split(/\./g).map(function(v) { return +v; }) :
[1, 0, 0]; // browserify ships 1.0-compatible version of util.inspect
var INSPECT_SUPPORTS_OBJECT_RETVAL =
versionParts[0] > 0 ||
versionParts[1] > 11 ||
(versionParts[0] === 11 && versionParts[1] >= 14);
ModelBaseClass.prototype.inspect = function(depth) {
if (INSPECT_SUPPORTS_OBJECT_RETVAL)
return this.__data;
// Workaround for older versions
// See also https://github.com/joyent/node/commit/66280de133
return util.inspect(this.__data, {
showHidden: false,
depth: depth,
colors: false,
});
};
ModelBaseClass.mixin = function(anotherClass, options) {
if (typeof anotherClass === 'string') {
this.modelBuilder.mixins.applyMixin(this, anotherClass, options);
} else {
if (anotherClass.prototype instanceof ModelBaseClass) {
var props = anotherClass.definition.properties;
for (var i in props) {
if (this.definition.properties[i]) {
continue;
}
this.defineProperty(i, props[i]);
}
}
return jutil.mixin(this, anotherClass, options);
}
};
ModelBaseClass.prototype.getDataSource = function() {
return this.__dataSource || this.constructor.dataSource;
};
ModelBaseClass.getDataSource = function() {
return this.dataSource;
};
ModelBaseClass.prototype.setStrict = function(strict) {
this.__strict = strict;
};
// Mixin observer
jutil.mixin(ModelBaseClass, require('./observer'));
jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 1 1 1 1 1 6 6 6 6 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var async = require('async');
var utils = require('./utils');
module.exports = ObserverMixin;
/**
* ObserverMixin class. Use to add observe/notifyObserversOf APIs to other
* classes.
*
* @class ObserverMixin
*/
function ObserverMixin() {
}
/**
* Register an asynchronous observer for the given operation (event).
* @param {String} operation The operation name.
* @callback {function} listener The listener function. It will be invoked with
* `this` set to the model constructor, e.g. `User`.
* @param {Object} context Operation-specific context.
* @param {function(Error=)} next The callback to call when the observer
* has finished.
* @end
*/
ObserverMixin.observe = function(operation, listener) {
this._observers = this._observers || {};
Eif (!this._observers[operation]) {
this._observers[operation] = [];
}
this._observers[operation].push(listener);
};
/**
* Unregister an asynchronous observer for the given operation (event).
* @param {String} operation The operation name.
* @callback {function} listener The listener function.
* @end
*/
ObserverMixin.removeObserver = function(operation, listener) {
if (!(this._observers && this._observers[operation])) return;
var index = this._observers[operation].indexOf(listener);
if (index !== -1) {
return this._observers[operation].splice(index, 1);
}
};
/**
* Unregister all asynchronous observers for the given operation (event).
* @param {String} operation The operation name.
* @end
*/
ObserverMixin.clearObservers = function(operation) {
if (!(this._observers && this._observers[operation])) return;
this._observers[operation].length = 0;
};
/**
* Invoke all async observers for the given operation(s).
* @param {String|String[]} operation The operation name(s).
* @param {Object} context Operation-specific context.
* @param {function(Error=)} callback The callback to call when all observers
* has finished.
*/
ObserverMixin.notifyObserversOf = function(operation, context, callback) {
var self = this;
if (!callback) callback = utils.createPromiseCallback();
function createNotifier(op) {
return function(ctx, done) {
if (typeof ctx === 'function' && done === undefined) {
done = ctx;
ctx = context;
}
self.notifyObserversOf(op, context, done);
};
}
if (Array.isArray(operation)) {
var tasks = [];
for (var i = 0, n = operation.length; i < n; i++) {
tasks.push(createNotifier(operation[i]));
}
return async.waterfall(tasks, callback);
}
var observers = this._observers && this._observers[operation];
this._notifyBaseObservers(operation, context, function doNotify(err) {
if (err) return callback(err, context);
if (!observers || !observers.length) return callback(null, context);
async.eachSeries(
observers,
function notifySingleObserver(fn, next) {
var retval = fn(context, next);
if (retval && typeof retval.then === 'function') {
retval.then(
function() { next(); return null; },
next // error handler
);
}
},
function(err) { callback(err, context); }
);
});
return callback.promise;
};
ObserverMixin._notifyBaseObservers = function(operation, context, callback) {
if (this.base && this.base.notifyObserversOf)
this.base.notifyObserversOf(operation, context, callback);
else
callback();
};
/**
* Run the given function with before/after observers. It's done in three serial
* steps asynchronously:
*
* - Notify the registered observers under 'before ' + operation
* - Execute the function
* - Notify the registered observers under 'after ' + operation
*
* If an error happens, it fails fast and calls the callback with err.
*
* @param {String} operation The operation name
* @param {Context} context The context object
* @param {Function} fn The task to be invoked as fn(done) or fn(context, done)
* @param {Function} callback The callback function
* @returns {*}
*/
ObserverMixin.notifyObserversAround = function(operation, context, fn, callback) {
var self = this;
context = context || {};
// Add callback to the context object so that an observer can skip other
// ones by calling the callback function directly and not calling next
if (context.end === undefined) {
context.end = callback;
}
// First notify before observers
return self.notifyObserversOf('before ' + operation, context,
function(err, context) {
if (err) return callback(err);
function cbForWork(err) {
var args = [].slice.call(arguments, 0);
if (err) return callback.apply(null, args);
// Find the list of params from the callback in addition to err
var returnedArgs = args.slice(1);
// Set up the array of results
context.results = returnedArgs;
// Notify after observers
self.notifyObserversOf('after ' + operation, context,
function(err, context) {
if (err) return callback(err, context);
var results = returnedArgs;
if (context && Array.isArray(context.results)) {
// Pickup the results from context
results = context.results;
}
// Build the list of params for final callback
var args = [err].concat(results);
callback.apply(null, args);
});
}
if (fn.length === 1) {
// fn(done)
fn(cbForWork);
} else {
// fn(context, done)
fn(context, cbForWork);
}
});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/*!
* Dependencies
*/
var assert = require('assert');
var util = require('util');
var async = require('async');
var utils = require('./utils');
var i8n = require('inflection');
var defineScope = require('./scope.js').defineScope;
var g = require('strong-globalize')();
var mergeQuery = utils.mergeQuery;
var idEquals = utils.idEquals;
var ModelBaseClass = require('./model.js');
var applyFilter = require('./connectors/memory').applyFilter;
var ValidationError = require('./validations.js').ValidationError;
var debug = require('debug')('loopback:relations');
var RelationTypes = {
belongsTo: 'belongsTo',
hasMany: 'hasMany',
hasOne: 'hasOne',
hasAndBelongsToMany: 'hasAndBelongsToMany',
referencesMany: 'referencesMany',
embedsOne: 'embedsOne',
embedsMany: 'embedsMany',
};
var RelationClasses = {
belongsTo: BelongsTo,
hasMany: HasMany,
hasManyThrough: HasManyThrough,
hasOne: HasOne,
hasAndBelongsToMany: HasAndBelongsToMany,
referencesMany: ReferencesMany,
embedsOne: EmbedsOne,
embedsMany: EmbedsMany,
};
exports.Relation = Relation;
exports.RelationDefinition = RelationDefinition;
exports.RelationTypes = RelationTypes;
exports.RelationClasses = RelationClasses;
exports.HasMany = HasMany;
exports.HasManyThrough = HasManyThrough;
exports.HasOne = HasOne;
exports.HasAndBelongsToMany = HasAndBelongsToMany;
exports.BelongsTo = BelongsTo;
exports.ReferencesMany = ReferencesMany;
exports.EmbedsOne = EmbedsOne;
exports.EmbedsMany = EmbedsMany;
function normalizeType(type) {
if (!type) {
return type;
}
var t1 = type.toLowerCase();
for (var t2 in RelationTypes) {
if (t2.toLowerCase() === t1) {
return t2;
}
}
return null;
};
function extendScopeMethods(definition, scopeMethods, ext) {
var customMethods = [];
var relationClass = RelationClasses[definition.type];
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
relationClass = RelationClasses.hasManyThrough;
}
if (typeof ext === 'function') {
customMethods = ext.call(definition, scopeMethods, relationClass);
} else if (typeof ext === 'object') {
function createFunc(definition, relationMethod) {
return function() {
var relation = new relationClass(definition, this);
return relationMethod.apply(relation, arguments);
};
};
for (var key in ext) {
var relationMethod = ext[key];
var method = scopeMethods[key] = createFunc(definition, relationMethod);
if (relationMethod.shared) {
sharedMethod(definition, key, method, relationMethod);
}
customMethods.push(key);
}
}
return [].concat(customMethods || []);
};
function bindRelationMethods(relation, relationMethod, definition) {
var methods = definition.methods || {};
Object.keys(methods).forEach(function(m) {
if (typeof methods[m] !== 'function') return;
relationMethod[m] = methods[m].bind(relation);
});
};
/**
* Relation definition class. Use to define relationships between models.
* @param {Object} definition
* @class RelationDefinition
*/
function RelationDefinition(definition) {
if (!(this instanceof RelationDefinition)) {
return new RelationDefinition(definition);
}
definition = definition || {};
this.name = definition.name;
assert(this.name, 'Relation name is missing');
this.type = normalizeType(definition.type);
assert(this.type, 'Invalid relation type: ' + definition.type);
this.modelFrom = definition.modelFrom;
assert(this.modelFrom, 'Source model is required');
this.keyFrom = definition.keyFrom;
this.modelTo = definition.modelTo;
this.keyTo = definition.keyTo;
this.polymorphic = definition.polymorphic;
if (typeof this.polymorphic !== 'object') {
assert(this.modelTo, 'Target model is required');
}
this.modelThrough = definition.modelThrough;
this.keyThrough = definition.keyThrough;
this.multiple = definition.multiple;
this.properties = definition.properties || {};
this.options = definition.options || {};
this.scope = definition.scope;
this.embed = definition.embed === true;
this.methods = definition.methods || {};
}
RelationDefinition.prototype.toJSON = function() {
var polymorphic = typeof this.polymorphic === 'object';
var modelToName = this.modelTo && this.modelTo.modelName;
if (!modelToName && polymorphic && this.type === 'belongsTo') {
modelToName = '<polymorphic>';
}
var json = {
name: this.name,
type: this.type,
modelFrom: this.modelFrom.modelName,
keyFrom: this.keyFrom,
modelTo: modelToName,
keyTo: this.keyTo,
multiple: this.multiple,
};
if (this.modelThrough) {
json.modelThrough = this.modelThrough.modelName;
json.keyThrough = this.keyThrough;
}
if (polymorphic) {
json.polymorphic = this.polymorphic;
}
return json;
};
/**
* Define a relation scope method
* @param {String} name of the method
* @param {Function} function to define
*/
RelationDefinition.prototype.defineMethod = function(name, fn) {
var relationClass = RelationClasses[this.type];
var relationName = this.name;
var modelFrom = this.modelFrom;
var definition = this;
var method;
if (definition.multiple) {
var scope = this.modelFrom.scopes[this.name];
if (!scope) throw new Error(g.f('Unknown relation {{scope}}: %s', this.name));
method = scope.defineMethod(name, function() {
var relation = new relationClass(definition, this);
return fn.apply(relation, arguments);
});
} else {
definition.methods[name] = fn;
method = function() {
var rel = this[relationName];
return rel[name].apply(rel, arguments);
};
}
if (method && fn.shared) {
sharedMethod(definition, name, method, fn);
modelFrom.prototype['__' + name + '__' + relationName] = method;
}
return method;
};
/**
* Apply the configured scope to the filter/query object.
* @param {Object} modelInstance
* @param {Object} filter (where, order, limit, fields, ...)
*/
RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
filter = filter || {};
filter.where = filter.where || {};
if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
typeof this.polymorphic === 'object') { // polymorphic
var discriminator = this.polymorphic.discriminator;
if (this.polymorphic.invert) {
filter.where[discriminator] = this.modelTo.modelName;
} else {
filter.where[discriminator] = this.modelFrom.modelName;
}
}
var scope;
if (typeof this.scope === 'function') {
scope = this.scope.call(this, modelInstance, filter);
} else {
scope = this.scope;
}
if (typeof scope === 'object') {
mergeQuery(filter, scope);
}
};
/**
* Apply the configured properties to the target object.
* @param {Object} modelInstance
* @param {Object} target
*/
RelationDefinition.prototype.applyProperties = function(modelInstance, obj) {
var source = modelInstance, target = obj;
if (this.options.invertProperties) {
source = obj;
target = modelInstance;
}
if (this.options.embedsProperties) {
target = target.__data[this.name] = {};
target[this.keyTo] = source[this.keyTo];
}
var k, key;
if (typeof this.properties === 'function') {
var data = this.properties.call(this, source, target);
for (k in data) {
target[k] = data[k];
}
} else if (Array.isArray(this.properties)) {
for (k = 0; k < this.properties.length; k++) {
key = this.properties[k];
target[key] = source[key];
}
} else if (typeof this.properties === 'object') {
for (k in this.properties) {
key = this.properties[k];
target[key] = source[k];
}
}
if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
typeof this.polymorphic === 'object') { // polymorphic
var discriminator = this.polymorphic.discriminator;
if (this.polymorphic.invert) {
target[discriminator] = this.modelTo.modelName;
} else {
target[discriminator] = this.modelFrom.modelName;
}
}
};
/**
* A relation attaching to a given model instance
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {Relation}
* @constructor
* @class Relation
*/
function Relation(definition, modelInstance) {
if (!(this instanceof Relation)) {
return new Relation(definition, modelInstance);
}
if (!(definition instanceof RelationDefinition)) {
definition = new RelationDefinition(definition);
}
this.definition = definition;
this.modelInstance = modelInstance;
}
Relation.prototype.resetCache = function(cache) {
cache = cache || undefined;
this.modelInstance.__cachedRelations[this.definition.name] = cache;
};
Relation.prototype.getCache = function() {
return this.modelInstance.__cachedRelations[this.definition.name];
};
Relation.prototype.callScopeMethod = function(methodName) {
var args = Array.prototype.slice.call(arguments, 1);
var modelInstance = this.modelInstance;
var rel = modelInstance[this.definition.name];
if (rel && typeof rel[methodName] === 'function') {
return rel[methodName].apply(rel, args);
} else {
throw new Error(g.f('Unknown scope method: %s', methodName));
}
};
/**
* Fetch the related model(s) - this is a helper method to unify access.
* @param (Boolean|Object} condOrRefresh refresh or conditions object
* @param {Object} [options] Options
* @param {Function} cb callback
*/
Relation.prototype.fetch = function(condOrRefresh, options, cb) {
this.modelInstance[this.definition.name].apply(this.modelInstance, arguments);
};
/**
* HasMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasMany}
* @constructor
* @class HasMany
*/
function HasMany(definition, modelInstance) {
if (!(this instanceof HasMany)) {
return new HasMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasMany);
Relation.apply(this, arguments);
}
util.inherits(HasMany, Relation);
HasMany.prototype.removeFromCache = function(id) {
var cache = this.modelInstance.__cachedRelations[this.definition.name];
var idName = this.definition.modelTo.definition.idName();
if (Array.isArray(cache)) {
for (var i = 0, n = cache.length; i < n; i++) {
if (idEquals(cache[i][idName], id)) {
return cache.splice(i, 1);
}
}
}
return null;
};
HasMany.prototype.addToCache = function(inst) {
if (!inst) {
return;
}
var cache = this.modelInstance.__cachedRelations[this.definition.name];
if (cache === undefined) {
cache = this.modelInstance.__cachedRelations[this.definition.name] = [];
}
var idName = this.definition.modelTo.definition.idName();
if (Array.isArray(cache)) {
for (var i = 0, n = cache.length; i < n; i++) {
if (idEquals(cache[i][idName], inst[idName])) {
cache[i] = inst;
return;
}
}
cache.push(inst);
}
};
/**
* HasManyThrough subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasManyThrough}
* @constructor
* @class HasManyThrough
*/
function HasManyThrough(definition, modelInstance) {
if (!(this instanceof HasManyThrough)) {
return new HasManyThrough(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasMany);
assert(definition.modelThrough);
HasMany.apply(this, arguments);
}
util.inherits(HasManyThrough, HasMany);
/**
* BelongsTo subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {BelongsTo}
* @constructor
* @class BelongsTo
*/
function BelongsTo(definition, modelInstance) {
if (!(this instanceof BelongsTo)) {
return new BelongsTo(definition, modelInstance);
}
assert(definition.type === RelationTypes.belongsTo);
Relation.apply(this, arguments);
}
util.inherits(BelongsTo, Relation);
/**
* HasAndBelongsToMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasAndBelongsToMany}
* @constructor
* @class HasAndBelongsToMany
*/
function HasAndBelongsToMany(definition, modelInstance) {
if (!(this instanceof HasAndBelongsToMany)) {
return new HasAndBelongsToMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasAndBelongsToMany);
Relation.apply(this, arguments);
}
util.inherits(HasAndBelongsToMany, Relation);
/**
* HasOne subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasOne}
* @constructor
* @class HasOne
*/
function HasOne(definition, modelInstance) {
if (!(this instanceof HasOne)) {
return new HasOne(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasOne);
Relation.apply(this, arguments);
}
util.inherits(HasOne, Relation);
/**
* EmbedsOne subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {EmbedsOne}
* @constructor
* @class EmbedsOne
*/
function EmbedsOne(definition, modelInstance) {
if (!(this instanceof EmbedsOne)) {
return new EmbedsOne(definition, modelInstance);
}
assert(definition.type === RelationTypes.embedsOne);
Relation.apply(this, arguments);
}
util.inherits(EmbedsOne, Relation);
/**
* EmbedsMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {EmbedsMany}
* @constructor
* @class EmbedsMany
*/
function EmbedsMany(definition, modelInstance) {
if (!(this instanceof EmbedsMany)) {
return new EmbedsMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.embedsMany);
Relation.apply(this, arguments);
}
util.inherits(EmbedsMany, Relation);
/**
* ReferencesMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {ReferencesMany}
* @constructor
* @class ReferencesMany
*/
function ReferencesMany(definition, modelInstance) {
if (!(this instanceof ReferencesMany)) {
return new ReferencesMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.referencesMany);
Relation.apply(this, arguments);
}
util.inherits(ReferencesMany, Relation);
/*!
* Find the relation by foreign key
* @param {*} foreignKey The foreign key
* @returns {Array} The array of matching relation objects
*/
function findBelongsTo(modelFrom, modelTo, keyTo) {
return Object.keys(modelFrom.relations)
.map(function(k) { return modelFrom.relations[k]; })
.filter(function(rel) {
return (rel.type === RelationTypes.belongsTo &&
rel.modelTo === modelTo &&
(keyTo === undefined || rel.keyTo === keyTo));
})
.map(function(rel) {
return rel.keyFrom;
});
}
/*!
* Look up a model by name from the list of given models
* @param {Object} models Models keyed by name
* @param {String} modelName The model name
* @returns {*} The matching model class
*/
function lookupModel(models, modelName) {
if (models[modelName]) {
return models[modelName];
}
var lookupClassName = modelName.toLowerCase();
for (var name in models) {
if (name.toLowerCase() === lookupClassName) {
return models[name];
}
}
}
function lookupModelTo(modelFrom, modelTo, params, singularize) {
if ('string' === typeof modelTo) {
var modelToName;
params.as = params.as || modelTo;
modelTo = params.model || modelTo;
if (typeof modelTo === 'string') {
modelToName = (singularize ? i8n.singularize(modelTo) : modelTo).toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo;
}
if (typeof modelTo === 'string') {
modelToName = (singularize ? i8n.singularize(params.as) : params.as).toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo;
}
if (typeof modelTo !== 'function') {
throw new Error(g.f('Could not find "%s" relation for %s', params.as, modelFrom.modelName));
}
}
return modelTo;
}
/*!
* Normalize polymorphic parameters
* @param {Object|String} params Name of the polymorphic relation or params
* @returns {Object} The normalized parameters
*/
function polymorphicParams(params, as) {
if (typeof params === 'string') params = {as: params};
if (typeof params.as !== 'string') params.as = as || 'reference'; // default
params.foreignKey = params.foreignKey || i8n.camelize(params.as + '_id', true);
params.discriminator = params.discriminator || i8n.camelize(params.as + '_type', true);
return params;
}
/**
* Define a "one to many" relationship by specifying the model name
*
* Examples:
* ```
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
* ```
*
* ```
* Book.hasMany(Chapter);
* ```
* Or, equivalently:
* ```
* Book.hasMany('chapters', {model: Chapter});
* ```
* @param {Model} modelFrom Source model class
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
var thisClassName = modelFrom.modelName;
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
var keyThrough = params.keyThrough || i8n.camelize(modelTo.modelName + '_id', true);
var pkName = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var discriminator, polymorphic;
if (params.polymorphic) {
polymorphic = polymorphicParams(params.polymorphic);
if (params.invert) {
polymorphic.invert = true;
keyThrough = polymorphic.foreignKey;
}
discriminator = polymorphic.discriminator;
if (!params.invert) {
fk = polymorphic.foreignKey;
}
if (!params.through) {
modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, {type: 'string', index: true});
}
}
var definition = new RelationDefinition({
name: relationName,
type: RelationTypes.hasMany,
modelFrom: modelFrom,
keyFrom: pkName,
keyTo: fk,
modelTo: modelTo,
multiple: true,
properties: params.properties,
scope: params.scope,
options: params.options,
keyThrough: keyThrough,
polymorphic: polymorphic,
});
definition.modelThrough = params.through;
modelFrom.relations[relationName] = definition;
if (!params.through) {
// obviously, modelTo should have attribute called `fk`
// for polymorphic relations, it is assumed to share the same fk type for all
// polymorphic models
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName, pkName);
}
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists'),
};
var findByIdFunc = scopeMethods.findById;
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
var destroyByIdFunc = scopeMethods.destroy;
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
var updateByIdFunc = scopeMethods.updateById;
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
var existsByIdFunc = scopeMethods.exists;
modelFrom.prototype['__exists__' + relationName] = existsByIdFunc;
if (definition.modelThrough) {
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.add = scopeMethod(definition, 'add');
scopeMethods.remove = scopeMethod(definition, 'remove');
var addFunc = scopeMethods.add;
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
} else {
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
}
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
for (var i = 0; i < customMethods.length; i++) {
var methodName = customMethods[i];
var method = scopeMethods[methodName];
if (typeof method === 'function' && method.shared === true) {
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
}
};
// Mix the property and scoped methods into the prototype class
defineScope(modelFrom.prototype, params.through || modelTo, relationName, function() {
var filter = {};
filter.where = {};
filter.where[fk] = this[pkName];
definition.applyScope(this, filter);
if (definition.modelThrough) {
var throughRelationName;
// find corresponding belongsTo relations from through model as collect
for (var r in definition.modelThrough.relations) {
var relation = definition.modelThrough.relations[r];
// should be a belongsTo and match modelTo and keyThrough
// if relation is polymorphic then check keyThrough only
if (relation.type === RelationTypes.belongsTo &&
(relation.polymorphic && !relation.modelTo || relation.modelTo === definition.modelTo) &&
(relation.keyFrom === definition.keyThrough)
) {
throughRelationName = relation.name;
break;
}
}
if (definition.polymorphic && definition.polymorphic.invert) {
filter.collect = definition.polymorphic.as;
filter.include = filter.collect;
} else {
filter.collect = throughRelationName || i8n.camelize(modelTo.modelName, true);
filter.include = filter.collect;
}
}
return filter;
}, scopeMethods, definition.options);
return definition;
};
function scopeMethod(definition, methodName) {
var relationClass = RelationClasses[definition.type];
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
relationClass = RelationClasses.hasManyThrough;
}
var method = function() {
var relation = new relationClass(definition, this);
return relation[methodName].apply(relation, arguments);
};
var relationMethod = relationClass.prototype[methodName];
if (relationMethod.shared) {
sharedMethod(definition, methodName, method, relationMethod);
}
return method;
}
function sharedMethod(definition, methodName, method, relationMethod) {
method.shared = true;
method.accepts = relationMethod.accepts;
method.returns = relationMethod.returns;
method.http = relationMethod.http;
method.description = relationMethod.description;
}
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
* @param {Function} cb The callback function
*/
HasMany.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var modelFrom = this.definition.modelFrom;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var idName = this.definition.modelTo.definition.idName();
var filter = {};
filter.where = {};
filter.where[idName] = fkId;
filter.where[fk] = modelInstance[pk];
cb = cb || utils.createPromiseCallback();
if (filter.where[fk] === undefined) {
// Foreign key is undefined
process.nextTick(cb);
return cb.promise;
}
this.definition.applyScope(modelInstance, filter);
modelTo.findOne(filter, options, function(err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
err = new Error(g.f('No instance with {{id}} %s found for %s', fkId, modelTo.modelName));
err.statusCode = 404;
return cb(err);
}
// Check if the foreign key matches the primary key
if (inst[fk] != null && idEquals(inst[fk], modelInstance[pk])) {
cb(null, inst);
} else {
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
modelFrom.modelName, pk, modelInstance[pk], modelTo.modelName, fk, inst[fk]));
err.statusCode = 400;
cb(err);
}
});
return cb.promise;
};
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
* @param {Function} cb The callback function
*/
HasMany.prototype.exists = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
cb = cb || utils.createPromiseCallback();
this.findById(fkId, function(err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, false);
}
// Check if the foreign key matches the primary key
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
cb(null, true);
} else {
cb(null, false);
}
});
return cb.promise;
};
/**
* Update a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} Changes to the data
* @param {Object} [options] Options
* @param {Function} cb The callback function
*/
HasMany.prototype.updateById = function(fkId, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
this.findById(fkId, options, function(err, inst) {
if (err) {
return cb && cb(err);
}
inst.updateAttributes(data, options, cb);
});
return cb.promise;
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
* @param {Function} cb The callback function
*/
HasMany.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var self = this;
this.findById(fkId, options, function(err, inst) {
if (err) {
return cb(err);
}
self.removeFromCache(fkId);
inst.destroy(options, cb);
});
return cb.promise;
};
var throughKeys = function(definition) {
var modelThrough = definition.modelThrough;
var pk2 = definition.modelTo.definition.idName();
let fk1, fk2;
if (typeof definition.polymorphic === 'object') { // polymorphic
fk1 = definition.keyTo;
if (definition.polymorphic.invert) {
fk2 = definition.polymorphic.foreignKey;
} else {
fk2 = definition.keyThrough;
}
} else if (definition.modelFrom === definition.modelTo) {
return findBelongsTo(modelThrough, definition.modelTo, pk2).
sort(function(fk1, fk2) {
// Fix for bug - https://github.com/strongloop/loopback-datasource-juggler/issues/571
// Make sure that first key is mapped to modelFrom
// & second key to modelTo. Order matters
return (definition.keyTo === fk1) ? -1 : 1;
});
} else {
fk1 = findBelongsTo(modelThrough, definition.modelFrom,
definition.keyFrom)[0];
fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2)[0];
}
return [fk1, fk2];
};
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key value
* @param {Object} [options] Options
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
cb = cb || utils.createPromiseCallback();
self.exists(fkId, options, function(err, exists) {
if (err || !exists) {
if (!err) {
err = new Error(g.f('No relation found in %s' +
' for (%s.%s,%s.%s)',
modelThrough.modelName, self.definition.modelFrom.modelName,
modelInstance[pk], modelTo.modelName, fkId));
err.statusCode = 404;
}
return cb(err);
}
modelTo.findById(fkId, options, function(err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
err = new Error(g.f('No instance with id %s found for %s', fkId, modelTo.modelName));
err.statusCode = 404;
return cb(err);
}
cb(err, inst);
});
});
return cb.promise;
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
cb = cb || utils.createPromiseCallback();
self.exists(fkId, options, function(err, exists) {
if (err || !exists) {
if (!err) {
err = new Error(g.f('No record found in %s for (%s.%s ,%s.%s)',
modelThrough.modelName, self.definition.modelFrom.modelName,
modelInstance[pk], modelTo.modelName, fkId));
err.statusCode = 404;
}
return cb(err);
}
self.remove(fkId, options, function(err) {
if (err) {
return cb(err);
}
modelTo.deleteById(fkId, options, cb);
});
});
return cb.promise;
};
// Create an instance of the target model and connect it to the instance of
// the source model by creating an instance of the through model
HasManyThrough.prototype.create = function create(data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
var definition = this.definition;
var modelTo = definition.modelTo;
var modelThrough = definition.modelThrough;
if (typeof data === 'function' && !cb) {
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
var modelInstance = this.modelInstance;
// First create the target model
modelTo.create(data, options, function(err, to) {
if (err) {
return cb(err, to);
}
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
function createRelation(to, next) {
var d = {}, q = {}, filter = {where: q};
d[fk1] = q[fk1] = modelInstance[definition.keyFrom];
d[fk2] = q[fk2] = to[pk2];
definition.applyProperties(modelInstance, d);
definition.applyScope(modelInstance, filter);
// Then create the through model
modelThrough.findOrCreate(filter, d, options, function(e, through) {
if (e) {
// Undo creation of the target model
to.destroy(options, function() {
next(e);
});
} else {
self.addToCache(to);
next(err, to);
}
});
}
// process array or single item
if (!Array.isArray(to))
createRelation(to, cb);
else
async.map(to, createRelation, cb);
});
return cb.promise;
};
/**
* Add the target model instance to the 'hasMany' relation
* @param {Object|ID} acInst The actual instance or id value
* @param {Object} [data] Optional data object for the through model to be created
* @param {Object} [options] Options
* @param {Function} [cb] Callback function
*/
HasManyThrough.prototype.add = function(acInst, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
if (typeof data === 'function') {
cb = data;
data = {};
}
var query = {};
data = data || {};
cb = cb || utils.createPromiseCallback();
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
query[fk1] = this.modelInstance[pk1];
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = {where: query};
definition.applyScope(this.modelInstance, filter);
data[fk1] = this.modelInstance[pk1];
data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
definition.applyProperties(this.modelInstance, data);
// Create an instance of the through model
modelThrough.findOrCreate(filter, data, options, function(err, ac) {
if (!err) {
if (acInst instanceof definition.modelTo) {
self.addToCache(acInst);
}
}
cb(err, ac);
});
return cb.promise;
};
/**
* Check if the target model instance is related to the 'hasMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
HasManyThrough.prototype.exists = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
var query = {};
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
query[fk1] = this.modelInstance[pk1];
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = {where: query};
definition.applyScope(this.modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelThrough.count(filter.where, options, function(err, ac) {
cb(err, ac > 0);
});
return cb.promise;
};
/**
* Remove the target model instance from the 'hasMany' relation
* @param {Object|ID) acInst The actual instance or id value
*/
HasManyThrough.prototype.remove = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
var query = {};
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
query[fk1] = this.modelInstance[pk1];
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = {where: query};
definition.applyScope(this.modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelThrough.deleteAll(filter.where, options, function(err) {
if (!err) {
self.removeFromCache(query[fk2]);
}
cb(err);
});
return cb.promise;
};
/**
* Declare "belongsTo" relation that sets up a one-to-one connection with
* another model, such that each instance of the declaring model "belongs to"
* one instance of the other model.
*
* For example, if an application includes users and posts, and each post can
* be written by exactly one user. The following code specifies that `Post` has
* a reference called `author` to the `User` model via the `userId` property of
* `Post` as the foreign key.
* ```
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
* ```
*
* This optional parameter default value is false, so the related object will
* be loaded from cache if available.
*
* @param {Class|String} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Name of foreign key property.
*
*/
RelationDefinition.belongsTo = function(modelFrom, modelTo, params) {
var discriminator, polymorphic;
params = params || {};
if ('string' === typeof modelTo && !params.polymorphic) {
modelTo = lookupModelTo(modelFrom, modelTo, params);
}
var pkName, relationName, fk;
if (params.polymorphic) {
relationName = params.as || (typeof modelTo === 'string' ? modelTo : null); // initially
if (params.polymorphic === true) {
// modelTo arg will be the name of the polymorphic relation (string)
polymorphic = polymorphicParams(modelTo, relationName);
} else {
polymorphic = polymorphicParams(params.polymorphic, relationName);
}
modelTo = null; // will lookup dynamically
pkName = params.primaryKey || params.idName || 'id';
relationName = params.as || polymorphic.as; // finally
fk = polymorphic.foreignKey;
discriminator = polymorphic.discriminator;
if (polymorphic.idType) { // explicit key type
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {type: polymorphic.idType, index: true});
} else { // try to use the same foreign key type as modelFrom
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName, pkName);
}
modelFrom.dataSource.defineProperty(modelFrom.modelName, discriminator, {type: 'string', index: true});
} else {
pkName = params.primaryKey || modelTo.dataSource.idName(modelTo.modelName) || 'id';
relationName = params.as || i8n.camelize(modelTo.modelName, true);
fk = params.foreignKey || relationName + 'Id';
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName, pkName);
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.belongsTo,
modelFrom: modelFrom,
keyFrom: fk,
keyTo: pkName,
modelTo: modelTo,
multiple: false,
properties: params.properties,
scope: params.scope,
options: params.options,
polymorphic: polymorphic,
methods: params.methods,
});
// Define a property for the scope so that we have 'this' for the scoped methods
Object.defineProperty(modelFrom.prototype, relationName, {
enumerable: true,
configurable: true,
get: function() {
var relation = new BelongsTo(definition, this);
var relationMethod = relation.related.bind(relation);
relationMethod.getAsync = relation.getAsync.bind(relation);
relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation);
if (!polymorphic) {
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName;
}
bindRelationMethods(relation, relationMethod, definition);
return relationMethod;
},
});
// FIXME: [rfeng] Wrap the property into a function for remoting
// so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
// For example, /api/orders/1/customer
var fn = function() {
var f = this[relationName];
f.apply(this, arguments);
};
modelFrom.prototype['__get__' + relationName] = fn;
return definition;
};
BelongsTo.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
var modelTo = this.definition.modelTo;
var fk = this.definition.keyFrom;
var pk = this.definition.keyTo;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
cb = cb || utils.createPromiseCallback();
this.definition.applyProperties(modelInstance, targetModelData || {});
modelTo.create(targetModelData, options, function(err, targetModel) {
if (!err) {
modelInstance[fk] = targetModel[pk];
if (modelInstance.isNewRecord()) {
self.resetCache(targetModel);
cb && cb(err, targetModel);
} else {
modelInstance.save(options, function(err, inst) {
if (cb && err) return cb && cb(err);
self.resetCache(targetModel);
cb && cb(err, targetModel);
});
}
} else {
cb && cb(err);
}
});
return cb.promise;
};
BelongsTo.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
this.definition.applyProperties(this.modelInstance, targetModelData || {});
return new modelTo(targetModelData);
};
BelongsTo.prototype.update = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
this.fetch(options, function(err, inst) {
if (inst instanceof ModelBaseClass) {
inst.updateAttributes(targetModelData, options, cb);
} else {
cb(new Error(g.f('{{BelongsTo}} relation %s is empty', definition.name)));
}
});
return cb.promise;
};
BelongsTo.prototype.destroy = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var definition = this.definition;
var modelInstance = this.modelInstance;
var fk = definition.keyFrom;
cb = cb || utils.createPromiseCallback();
this.fetch(options, function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
modelInstance[fk] = null;
modelInstance.save(options, function(err, targetModel) {
if (cb && err) return cb && cb(err);
cb && cb(err, targetModel);
});
} else {
cb(new Error(g.f('{{BelongsTo}} relation %s is empty', definition.name)));
}
});
return cb.promise;
};
/**
* Define the method for the belongsTo relation itself
* It will support one of the following styles:
* - order.customer(refresh, options, callback): Load the target model instance asynchronously
* - order.customer(customer): Synchronous setter of the target model instance
* - order.customer(): Synchronous getter of the target model instance
*
* @param refresh
* @param params
* @returns {*}
*/
BelongsTo.prototype.related = function(condOrRefresh, options, cb) {
var self = this;
var modelFrom = this.definition.modelFrom;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var discriminator;
var scopeQuery = null;
var newValue;
if ((condOrRefresh instanceof ModelBaseClass) &&
options === undefined && cb === undefined) {
// order.customer(customer)
newValue = condOrRefresh;
condOrRefresh = false;
} else if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// order.customer(cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
// order.customer(condOrRefresh, cb)
cb = options;
options = {};
}
if (!newValue) {
scopeQuery = condOrRefresh;
}
if (typeof this.definition.polymorphic === 'object') {
discriminator = this.definition.polymorphic.discriminator;
}
var cachedValue;
if (!condOrRefresh) {
cachedValue = self.getCache();
}
if (newValue) { // acts as setter
modelInstance[fk] = newValue[pk];
if (discriminator) {
modelInstance[discriminator] = newValue.constructor.modelName;
}
this.definition.applyProperties(modelInstance, newValue);
self.resetCache(newValue);
} else if (typeof cb === 'function') { // acts as async getter
if (discriminator) {
var modelToName = modelInstance[discriminator];
if (typeof modelToName !== 'string') {
throw new Error(g.f('{{Polymorphic}} model not found: `%s` not set', discriminator));
}
modelToName = modelToName.toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
if (!modelTo) {
throw new Error(g.f('{{Polymorphic}} model not found: `%s`', modelToName));
}
}
if (cachedValue === undefined || !(cachedValue instanceof ModelBaseClass)) {
var query = {where: {}};
query.where[pk] = modelInstance[fk];
if (query.where[pk] === undefined || query.where[pk] === null) {
// Foreign key is undefined
return process.nextTick(cb);
}
this.definition.applyScope(modelInstance, query);
if (scopeQuery) mergeQuery(query, scopeQuery);
if (Array.isArray(query.fields) && query.fields.indexOf(pk) === -1) {
query.fields.push(pk); // always include the pk
}
modelTo.findOne(query, options, function(err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, null);
}
// Check if the foreign key matches the primary key
if (inst[pk] != null && modelInstance[fk] != null &&
inst[pk].toString() === modelInstance[fk].toString()) {
self.resetCache(inst);
cb(null, inst);
} else {
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
self.definition.modelFrom.modelName, fk, modelInstance[fk],
modelTo.modelName, pk, inst[pk]));
err.statusCode = 400;
cb(err);
}
});
return modelInstance[fk];
} else {
cb(null, cachedValue);
return cachedValue;
}
} else if (condOrRefresh === undefined) { // acts as sync getter
return cachedValue;
} else { // setter
modelInstance[fk] = newValue;
self.resetCache();
}
};
/**
* Define a Promise-based method for the belongsTo relation itself
* - order.customer.get(cb): Load the target model instance asynchronously
*
* @param {Function} cb Callback of the form function (err, inst)
* @returns {Promise | Undefined} returns promise if callback is omitted
*/
BelongsTo.prototype.getAsync = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
// order.customer.getAsync(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
this.related(true, options, cb);
return cb.promise;
};
/**
* A hasAndBelongsToMany relation creates a direct many-to-many connection with
* another model, with no intervening model. For example, if your application
* includes users and groups, with each group having many users and each user
* appearing in many groups, you could declare the models this way:
* ```
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
* ```
*
* @param {String|Object} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelTo, params) {
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
var models = modelFrom.dataSource.modelBuilder.models;
if (!params.through) {
if (params.polymorphic) throw new Error(g.f('{{Polymorphic}} relations need a through model'));
if (params.throughTable) {
params.through = modelFrom.dataSource.define(params.throughTable);
} else {
var name1 = modelFrom.modelName + modelTo.modelName;
var name2 = modelTo.modelName + modelFrom.modelName;
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
modelFrom.dataSource.define(name1);
}
}
var options = {as: params.as, through: params.through};
options.properties = params.properties;
options.scope = params.scope;
// Forward relation options like "disableInclude"
options.options = params.options;
if (params.polymorphic) {
var polymorphic = polymorphicParams(params.polymorphic);
options.polymorphic = polymorphic; // pass through
var accessor = params.through.prototype[polymorphic.as];
if (typeof accessor !== 'function') { // declare once
// use the name of the polymorphic rel, not modelTo
params.through.belongsTo(polymorphic.as, {polymorphic: true});
}
} else {
params.through.belongsTo(modelFrom);
}
params.through.belongsTo(modelTo);
return this.hasMany(modelFrom, modelTo, options);
};
/**
* A HasOne relation creates a one-to-one connection from modelFrom to modelTo.
* This relation indicates that each instance of a model contains or possesses
* one instance of another model. For example, each supplier in your application
* has only one account.
*
* @param {Function} modelFrom The declaring model class
* @param {String|Function} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationDefinition.hasOne = function(modelFrom, modelTo, params) {
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params);
var pk = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
var discriminator, polymorphic;
if (params.polymorphic) {
polymorphic = polymorphicParams(params.polymorphic);
fk = polymorphic.foreignKey;
discriminator = polymorphic.discriminator;
if (!params.through) {
modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, {type: 'string', index: true});
}
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.hasOne,
modelFrom: modelFrom,
keyFrom: pk,
keyTo: fk,
modelTo: modelTo,
multiple: false,
properties: params.properties,
scope: params.scope,
options: params.options,
polymorphic: polymorphic,
methods: params.methods,
});
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName, pk);
// Define a property for the scope so that we have 'this' for the scoped methods
Object.defineProperty(modelFrom.prototype, relationName, {
enumerable: true,
configurable: true,
get: function() {
var relation = new HasOne(definition, this);
var relationMethod = relation.related.bind(relation);
relationMethod.getAsync = relation.getAsync.bind(relation);
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName;
bindRelationMethods(relation, relationMethod, definition);
return relationMethod;
},
});
// FIXME: [rfeng] Wrap the property into a function for remoting
// so that it can be accessed as /api/<model>/<id>/<hasOneRelationName>
// For example, /api/orders/1/customer
modelFrom.prototype['__get__' + relationName] = function() {
var f = this[relationName];
f.apply(this, arguments);
};
modelFrom.prototype['__create__' + relationName] = function() {
var f = this[relationName].create;
f.apply(this, arguments);
};
modelFrom.prototype['__update__' + relationName] = function() {
var f = this[relationName].update;
f.apply(this, arguments);
};
modelFrom.prototype['__destroy__' + relationName] = function() {
var f = this[relationName].destroy;
f.apply(this, arguments);
};
return definition;
};
/**
* Create a target model instance
* @param {Object} targetModelData The target model data
* @callback {Function} [cb] Callback function
* @param {String|Object} err Error string or object
* @param {Object} The newly created target model instance
*/
HasOne.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.profile.create(options, cb)
cb = options;
options = {};
}
var self = this;
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
targetModelData[fk] = modelInstance[pk];
var query = {where: {}};
query.where[fk] = targetModelData[fk];
this.definition.applyScope(modelInstance, query);
this.definition.applyProperties(modelInstance, targetModelData);
modelTo.findOrCreate(query, targetModelData, options,
function(err, targetModel, created) {
if (err) {
return cb && cb(err);
}
if (created) {
// Refresh the cache
self.resetCache(targetModel);
cb && cb(err, targetModel);
} else {
cb && cb(new Error(g.f(
'{{HasOne}} relation cannot create more than one instance of %s',
modelTo.modelName)));
}
});
return cb.promise;
};
HasOne.prototype.update = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.profile.update(data, cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
var fk = this.definition.keyTo;
this.fetch(function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
delete targetModelData[fk];
targetModel.updateAttributes(targetModelData, options, cb);
} else {
cb(new Error(g.f('{{HasOne}} relation %s is empty', definition.name)));
}
});
return cb.promise;
};
HasOne.prototype.destroy = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.profile.destroy(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
this.fetch(function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
targetModel.destroy(options, cb);
} else {
cb(new Error(g.f('{{HasOne}} relation %s is empty', definition.name)));
}
});
return cb.promise;
};
/**
* Create a target model instance
* @param {Object} targetModelData The target model data
* @callback {Function} [cb] Callback function
* @param {String|Object} err Error string or object
* @param {Object} The newly created target model instance
*/
HasMany.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.create(data, cb)
cb = options;
options = {};
}
var self = this;
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var fkAndProps = function(item) {
item[fk] = modelInstance[pk];
self.definition.applyProperties(modelInstance, item);
};
var apply = function(data, fn) {
if (Array.isArray(data)) {
data.forEach(fn);
} else {
fn(data);
}
};
apply(targetModelData, fkAndProps);
modelTo.create(targetModelData, options, function(err, targetModel) {
if (!err) {
// Refresh the cache
apply(targetModel, self.addToCache.bind(self));
cb && cb(err, targetModel);
} else {
cb && cb(err);
}
});
return cb.promise;
};
/**
* Build a target model instance
* @param {Object} targetModelData The target model data
* @returns {Object} The newly built target model instance
*/
HasMany.prototype.build = HasOne.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var fk = this.definition.keyTo;
targetModelData = targetModelData || {};
targetModelData[fk] = this.modelInstance[pk];
this.definition.applyProperties(this.modelInstance, targetModelData);
return new modelTo(targetModelData);
};
/**
* Define the method for the hasOne relation itself
* It will support one of the following styles:
* - order.customer(refresh, callback): Load the target model instance asynchronously
* - order.customer(customer): Synchronous setter of the target model instance
* - order.customer(): Synchronous getter of the target model instance
*
* @param {Boolean} refresh Reload from the data source
* @param {Object|Function} params Query parameters
* @returns {Object}
*/
HasOne.prototype.related = function(condOrRefresh, options, cb) {
var self = this;
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var definition = this.definition;
var modelInstance = this.modelInstance;
var newValue;
if ((condOrRefresh instanceof ModelBaseClass) &&
options === undefined && cb === undefined) {
// order.customer(customer)
newValue = condOrRefresh;
condOrRefresh = false;
} else if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// customer.profile(cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
// customer.profile(condOrRefresh, cb)
cb = options;
options = {};
}
var cachedValue;
if (!condOrRefresh) {
cachedValue = self.getCache();
}
if (newValue) { // acts as setter
newValue[fk] = modelInstance[pk];
self.resetCache(newValue);
} else if (typeof cb === 'function') { // acts as async getter
if (cachedValue === undefined) {
var query = {where: {}};
query.where[fk] = modelInstance[pk];
definition.applyScope(modelInstance, query);
modelTo.findOne(query, options, function(err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, null);
}
// Check if the foreign key matches the primary key
if (inst[fk] != null && modelInstance[pk] != null &&
inst[fk].toString() === modelInstance[pk].toString()) {
self.resetCache(inst);
cb(null, inst);
} else {
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
self.definition.modelFrom.modelName, pk, modelInstance[pk],
modelTo.modelName, fk, inst[fk]));
err.statusCode = 400;
cb(err);
}
});
return modelInstance[pk];
} else {
cb(null, cachedValue);
return cachedValue;
}
} else if (condOrRefresh === undefined) { // acts as sync getter
return cachedValue;
} else { // setter
newValue[fk] = modelInstance[pk];
self.resetCache();
}
};
/**
* Define a Promise-based method for the hasOne relation itself
* - order.customer.getAsync(cb): Load the target model instance asynchronously
*
* @param {Function} cb Callback of the form function (err, inst)
* @returns {Promise | Undefined} Returns promise if cb is omitted
*/
HasOne.prototype.getAsync = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
// order.profile.getAsync(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
this.related(true, cb);
return cb.promise;
};
RelationDefinition.embedsOne = function(modelFrom, modelTo, params) {
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params);
var thisClassName = modelFrom.modelName;
var relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'Item');
var propertyName = params.property || i8n.camelize(modelTo.modelName, true);
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
if (relationName === propertyName) {
propertyName = '_' + propertyName;
debug('EmbedsOne property cannot be equal to relation name: ' +
'forcing property %s for relation %s', propertyName, relationName);
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.embedsOne,
modelFrom: modelFrom,
keyFrom: propertyName,
keyTo: idName,
modelTo: modelTo,
multiple: false,
properties: params.properties,
scope: params.scope,
options: params.options,
embed: true,
methods: params.methods,
});
var opts = {type: modelTo};
if (params.default === true) {
opts.default = function() { return new modelTo(); };
} else if (typeof params.default === 'object') {
opts.default = (function(def) {
return function() {
return new modelTo(def);
};
}(params.default));
}
modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts);
// validate the embedded instance
if (definition.options.validate !== false) {
modelFrom.validate(relationName, function(err) {
var inst = this[propertyName];
if (inst instanceof modelTo) {
if (!inst.isValid()) {
var first = Object.keys(inst.errors)[0];
var msg = 'is invalid: `' + first + '` ' + inst.errors[first];
this.errors.add(relationName, msg, 'invalid');
err(false);
}
}
});
}
// Define a property for the scope so that we have 'this' for the scoped methods
Object.defineProperty(modelFrom.prototype, relationName, {
enumerable: true,
configurable: true,
get: function() {
var relation = new EmbedsOne(definition, this);
var relationMethod = relation.related.bind(relation);
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation);
relationMethod.value = relation.embeddedValue.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName;
bindRelationMethods(relation, relationMethod, definition);
return relationMethod;
},
});
// FIXME: [rfeng] Wrap the property into a function for remoting
// so that it can be accessed as /api/<model>/<id>/<embedsOneRelationName>
// For example, /api/orders/1/customer
modelFrom.prototype['__get__' + relationName] = function() {
var f = this[relationName];
f.apply(this, arguments);
};
modelFrom.prototype['__create__' + relationName] = function() {
var f = this[relationName].create;
f.apply(this, arguments);
};
modelFrom.prototype['__update__' + relationName] = function() {
var f = this[relationName].update;
f.apply(this, arguments);
};
modelFrom.prototype['__destroy__' + relationName] = function() {
var f = this[relationName].destroy;
f.apply(this, arguments);
};
return definition;
};
EmbedsOne.prototype.related = function(condOrRefresh, options, cb) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
var newValue;
if ((condOrRefresh instanceof ModelBaseClass) &&
options === undefined && cb === undefined) {
// order.customer(customer)
newValue = condOrRefresh;
condOrRefresh = false;
} else if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// order.customer(cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
// order.customer(condOrRefresh, cb)
cb = options;
options = {};
}
if (newValue) { // acts as setter
if (newValue instanceof modelTo) {
this.definition.applyProperties(modelInstance, newValue);
modelInstance.setAttribute(propertyName, newValue);
}
return;
}
var embeddedInstance = this.embeddedValue();
if (embeddedInstance) {
embeddedInstance.__persisted = true;
}
if (typeof cb === 'function') { // acts as async getter
process.nextTick(function() {
cb(null, embeddedInstance);
});
} else if (condOrRefresh === undefined) { // acts as sync getter
return embeddedInstance;
}
};
EmbedsOne.prototype.prepareEmbeddedInstance = function(inst) {
if (inst && inst.triggerParent !== 'function') {
var self = this;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (this.definition.options.persistent) {
var pk = this.definition.keyTo;
inst.__persisted = !!inst[pk];
} else {
inst.__persisted = true;
}
inst.triggerParent = function(actionName, callback) {
if (actionName === 'save') {
var embeddedValue = self.embeddedValue();
modelInstance.updateAttribute(propertyName,
embeddedValue, function(err, modelInst) {
callback(err, err ? null : modelInst);
});
} else if (actionName === 'destroy') {
modelInstance.unsetAttribute(propertyName, true);
// cannot delete property completely the way save works. operator $unset needed like mongo
modelInstance.save(function(err, modelInst) {
callback(err, modelInst);
});
} else {
process.nextTick(callback);
}
};
var originalTrigger = inst.trigger;
inst.trigger = function(actionName, work, data, callback) {
if (typeof work === 'function') {
var originalWork = work;
work = function(next) {
originalWork.call(this, function(done) {
inst.triggerParent(actionName, function(err, inst) {
next(done); // TODO [fabien] - error handling?
});
});
};
}
originalTrigger.call(this, actionName, work, data, callback);
};
}
};
EmbedsOne.prototype.embeddedValue = function(modelInstance) {
modelInstance = modelInstance || this.modelInstance;
var embeddedValue = modelInstance[this.definition.keyFrom];
this.prepareEmbeddedInstance(embeddedValue);
return embeddedValue;
};
EmbedsOne.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// order.customer.create(data, cb)
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var inst = this.callScopeMethod('build', targetModelData);
var updateEmbedded = function(callback) {
if (modelInstance.isNewRecord()) {
modelInstance.setAttribute(propertyName, inst);
modelInstance.save(options, function(err) {
callback(err, err ? null : inst);
});
} else {
modelInstance.updateAttribute(propertyName,
inst, options, function(err) {
callback(err, err ? null : inst);
});
}
};
if (this.definition.options.persistent) {
inst.save(options, function(err) { // will validate
if (err) return cb(err, inst);
updateEmbedded(cb);
});
} else {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) {
return process.nextTick(function() {
cb(err);
});
}
err = inst.isValid() ? null : new ValidationError(inst);
if (err) {
process.nextTick(function() {
cb(err);
});
} else {
updateEmbedded(function(err, inst) {
if (err) return cb(err);
context.instance = inst;
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, err ? null : inst);
});
});
}
});
}
return cb.promise;
};
EmbedsOne.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
var forceId = this.definition.options.forceId;
var persistent = this.definition.options.persistent;
var connector = modelTo.dataSource.connector;
targetModelData = targetModelData || {};
this.definition.applyProperties(modelInstance, targetModelData);
var pk = this.definition.keyTo;
var pkProp = modelTo.definition.properties[pk];
var assignId = (forceId || targetModelData[pk] === undefined);
assignId = assignId && !persistent && (pkProp && pkProp.generated);
if (assignId && typeof connector.generateId === 'function') {
var id = connector.generateId(modelTo.modelName, targetModelData, pk);
targetModelData[pk] = id;
}
var embeddedInstance = new modelTo(targetModelData);
modelInstance[propertyName] = embeddedInstance;
this.prepareEmbeddedInstance(embeddedInstance);
return embeddedInstance;
};
EmbedsOne.prototype.update = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// order.customer.update(data, cb)
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
var isInst = targetModelData instanceof ModelBaseClass;
var data = isInst ? targetModelData.toObject() : targetModelData;
var embeddedInstance = this.embeddedValue();
if (embeddedInstance instanceof modelTo) {
cb = cb || utils.createPromiseCallback();
var hookState = {};
var context = {
Model: modelTo,
currentInstance: embeddedInstance,
data: data,
options: options || {},
hookState: hookState,
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
embeddedInstance.setAttributes(context.data);
// TODO support async validations
if (!embeddedInstance.isValid()) {
return cb(new ValidationError(embeddedInstance));
}
modelInstance.save(function(err, inst) {
if (err) return cb(err);
context = {
Model: modelTo,
instance: inst ? inst[propertyName] : embeddedInstance,
options: options || {},
hookState: hookState,
};
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, context.instance);
});
});
});
} else if (!embeddedInstance && cb) {
return this.callScopeMethod('create', data, cb);
} else if (!embeddedInstance) {
return this.callScopeMethod('build', data);
}
return cb.promise;
};
EmbedsOne.prototype.destroy = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
// order.customer.destroy(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
var embeddedInstance = modelInstance[propertyName];
if (!embeddedInstance) {
cb();
return cb.promise;
}
modelInstance.unsetAttribute(propertyName, true);
var context = {
Model: modelTo,
instance: embeddedInstance,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before delete', context, function(err) {
if (err) return cb(err);
modelInstance.save(function(err, result) {
if (err) return cb(err);
modelTo.notifyObserversOf('after delete', context, cb);
});
});
return cb.promise;
};
RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) {
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
var thisClassName = modelFrom.modelName;
var relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List');
var propertyName = params.property || i8n.camelize(modelTo.pluralModelName, true);
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
if (relationName === propertyName) {
propertyName = '_' + propertyName;
debug('EmbedsMany property cannot be equal to relation name: ' +
'forcing property %s for relation %s', propertyName, relationName);
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.embedsMany,
modelFrom: modelFrom,
keyFrom: propertyName,
keyTo: idName,
modelTo: modelTo,
multiple: true,
properties: params.properties,
scope: params.scope,
options: params.options,
embed: true,
});
modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, {
type: [modelTo], default: function() { return []; },
});
if (typeof modelTo.dataSource.connector.generateId !== 'function') {
modelFrom.validate(propertyName, function(err) {
var self = this;
var embeddedList = this[propertyName] || [];
var hasErrors = false;
embeddedList.forEach(function(item, idx) {
if (item instanceof modelTo && item[idName] == undefined) {
hasErrors = true;
var msg = 'contains invalid item at index `' + idx + '`:';
msg += ' `' + idName + '` is blank';
self.errors.add(propertyName, msg, 'invalid');
}
});
if (hasErrors) err(false);
});
}
if (!params.polymorphic) {
modelFrom.validate(propertyName, function(err) {
var embeddedList = this[propertyName] || [];
var ids = embeddedList.map(function(m) { return m[idName] && m[idName].toString(); }); // mongodb
var uniqueIds = ids.filter(function(id, pos) {
return utils.findIndexOf(ids, id, idEquals) === pos;
});
if (ids.length !== uniqueIds.length) {
this.errors.add(propertyName, 'contains duplicate `' + idName + '`', 'uniqueness');
err(false);
}
}, {code: 'uniqueness'});
}
// validate all embedded items
if (definition.options.validate !== false) {
modelFrom.validate(propertyName, function(err) {
var self = this;
var embeddedList = this[propertyName] || [];
var hasErrors = false;
embeddedList.forEach(function(item, idx) {
if (item instanceof modelTo) {
if (!item.isValid()) {
hasErrors = true;
var id = item[idName];
var first = Object.keys(item.errors)[0];
let msg = id ?
'contains invalid item: `' + id + '`' :
'contains invalid item at index `' + idx + '`';
msg += ' (`' + first + '` ' + item.errors[first] + ')';
self.errors.add(propertyName, msg, 'invalid');
}
} else {
hasErrors = true;
self.errors.add(propertyName, 'contains invalid item', 'invalid');
}
});
if (hasErrors) err(false);
});
}
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists'),
add: scopeMethod(definition, 'add'),
remove: scopeMethod(definition, 'remove'),
get: scopeMethod(definition, 'get'),
set: scopeMethod(definition, 'set'),
unset: scopeMethod(definition, 'unset'),
at: scopeMethod(definition, 'at'),
value: scopeMethod(definition, 'embeddedValue'),
};
var findByIdFunc = scopeMethods.findById;
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
var destroyByIdFunc = scopeMethods.destroy;
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
var updateByIdFunc = scopeMethods.updateById;
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
var addFunc = scopeMethods.add;
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
scopeMethods.related = scopeMethod(definition, 'related'); // bound to definition
if (!definition.options.persistent) {
scopeMethods.destroyAll = scopeMethod(definition, 'destroyAll');
}
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
for (var i = 0; i < customMethods.length; i++) {
var methodName = customMethods[i];
var method = scopeMethods[methodName];
if (typeof method === 'function' && method.shared === true) {
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
}
};
// Mix the property and scoped methods into the prototype class
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
return {};
}, scopeMethods, definition.options);
scopeDefinition.related = scopeMethods.related;
return definition;
};
EmbedsMany.prototype.prepareEmbeddedInstance = function(inst) {
if (inst && inst.triggerParent !== 'function') {
var self = this;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (this.definition.options.persistent) {
var pk = this.definition.keyTo;
inst.__persisted = !!inst[pk];
} else {
inst.__persisted = true;
}
inst.triggerParent = function(actionName, callback) {
if (actionName === 'save' || actionName === 'destroy') {
var embeddedList = self.embeddedList();
if (actionName === 'destroy') {
var index = embeddedList.indexOf(inst);
if (index > -1) embeddedList.splice(index, 1);
}
modelInstance.updateAttribute(propertyName,
embeddedList, function(err, modelInst) {
callback(err, err ? null : modelInst);
});
} else {
process.nextTick(callback);
}
};
var originalTrigger = inst.trigger;
inst.trigger = function(actionName, work, data, callback) {
if (typeof work === 'function') {
var originalWork = work;
work = function(next) {
originalWork.call(this, function(done) {
inst.triggerParent(actionName, function(err, inst) {
next(done); // TODO [fabien] - error handling?
});
});
};
}
originalTrigger.call(this, actionName, work, data, callback);
};
}
};
EmbedsMany.prototype.embeddedList =
EmbedsMany.prototype.embeddedValue = function(modelInstance) {
modelInstance = modelInstance || this.modelInstance;
var embeddedList = modelInstance[this.definition.keyFrom] || [];
embeddedList.forEach(this.prepareEmbeddedInstance.bind(this));
return embeddedList;
};
EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var actualCond = {};
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// customer.emails(receiver, scopeParams, cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
// customer.emails(receiver, scopeParams, condOrRefresh, cb)
cb = options;
options = {};
}
if (typeof condOrRefresh === 'object') {
actualCond = condOrRefresh;
}
var embeddedList = this.embeddedList(receiver);
this.definition.applyScope(receiver, actualCond);
var params = mergeQuery(actualCond, scopeParams);
if (params.where && Object.keys(params.where).length > 0) { // TODO [fabien] Support order/sorting
embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList;
}
var returnRelated = function(list) {
if (params.include) {
modelTo.include(list, params.include, options, cb);
} else {
process.nextTick(function() { cb(null, list); });
}
};
returnRelated(embeddedList);
};
EmbedsMany.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// order.emails(fkId, cb)
cb = options;
options = {};
}
var pk = this.definition.keyTo;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
var find = function(id) {
for (var i = 0; i < embeddedList.length; i++) {
var item = embeddedList[i];
if (idEquals(item[pk], id)) return item;
}
return null;
};
var item = find(fkId.toString()); // in case of explicit id
item = (item instanceof modelTo) ? item : null;
if (typeof cb === 'function') {
process.nextTick(function() {
cb(null, item);
});
};
return item; // sync
};
EmbedsMany.prototype.exists = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.emails.exists(fkId, cb)
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var inst = this.findById(fkId, options, function(err, inst) {
if (cb) cb(err, inst instanceof modelTo);
});
return inst instanceof modelTo; // sync
};
EmbedsMany.prototype.updateById = function(fkId, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.emails.updateById(fkId, data, cb)
cb = options;
options = {};
}
if (typeof data === 'function') {
// customer.emails.updateById(fkId, cb)
cb = data;
data = {};
}
options = options || {};
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
var inst = this.findById(fkId);
if (inst instanceof modelTo) {
var hookState = {};
var context = {
Model: modelTo,
currentInstance: inst,
data: data,
options: options,
hookState: hookState,
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb && cb(err);
inst.setAttributes(data);
err = inst.isValid() ? null : new ValidationError(inst);
if (err && typeof cb === 'function') {
return process.nextTick(function() {
cb(err, inst);
});
}
context = {
Model: modelTo,
instance: inst,
options: options,
hookState: hookState,
};
if (typeof cb === 'function') {
modelInstance.updateAttribute(propertyName, embeddedList, options,
function(err) {
if (err) return cb(err, inst);
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, inst);
});
});
} else {
modelTo.notifyObserversOf('after save', context, function(err) {
if (!err) return;
debug('Unhandled error in "after save" hooks: %s', err.stack || err);
});
}
});
} else if (typeof cb === 'function') {
process.nextTick(function() {
cb(null, null); // not found
});
}
return inst; // sync
};
EmbedsMany.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.emails.destroyById(fkId, cb)
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
var inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId);
if (inst instanceof modelTo) {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before delete', context, function(err) {
if (err) return cb(err);
var index = embeddedList.indexOf(inst);
if (index > -1) embeddedList.splice(index, 1);
if (typeof cb !== 'function') return;
modelInstance.updateAttribute(propertyName,
embeddedList, function(err) {
if (err) return cb(err);
modelTo.notifyObserversOf('after delete', context, function(err) {
cb(err);
});
});
});
} else if (typeof cb === 'function') {
process.nextTick(cb); // not found
}
return inst; // sync
};
EmbedsMany.prototype.destroyAll = function(where, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.emails.destroyAll(where, cb);
cb = options;
options = {};
} else if (typeof where === 'function' &&
options === undefined && cb === undefined) {
// customer.emails.destroyAll(cb);
cb = where;
where = {};
}
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
if (where && Object.keys(where).length > 0) {
var filter = applyFilter({where: where});
var reject = function(v) { return !filter(v); };
embeddedList = embeddedList ? embeddedList.filter(reject) : embeddedList;
} else {
embeddedList = [];
}
if (typeof cb === 'function') {
modelInstance.updateAttribute(propertyName,
embeddedList, function(err) {
cb(err);
});
} else {
modelInstance.setAttribute(propertyName, embeddedList);
}
};
EmbedsMany.prototype.get = EmbedsMany.prototype.findById;
EmbedsMany.prototype.set = EmbedsMany.prototype.updateById;
EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById;
EmbedsMany.prototype.at = function(index, cb) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
var item = embeddedList[parseInt(index)];
item = (item instanceof modelTo) ? item : null;
if (typeof cb === 'function') {
process.nextTick(function() {
cb(null, item);
});
};
return item; // sync
};
EmbedsMany.prototype.create = function(targetModelData, options, cb) {
var pk = this.definition.keyTo;
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof options === 'function' && cb === undefined) {
// customer.emails.create(cb)
cb = options;
options = {};
}
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var embeddedList = this.embeddedList();
var inst = this.callScopeMethod('build', targetModelData);
var updateEmbedded = function(callback) {
if (modelInstance.isNewRecord()) {
modelInstance.setAttribute(propertyName, embeddedList);
modelInstance.save(options, function(err) {
callback(err, err ? null : inst);
});
} else {
modelInstance.updateAttribute(propertyName,
embeddedList, options, function(err) {
callback(err, err ? null : inst);
});
}
};
if (this.definition.options.persistent) {
inst.save(function(err) { // will validate
if (err) return cb(err, inst);
updateEmbedded(cb);
});
} else {
const err = inst.isValid() ? null : new ValidationError(inst);
if (err) {
process.nextTick(function() {
cb(err);
});
} else {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
updateEmbedded(function(err, inst) {
if (err) return cb(err, null);
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, err ? null : inst);
});
});
});
}
}
return cb.promise;
};
EmbedsMany.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var forceId = this.definition.options.forceId;
var persistent = this.definition.options.persistent;
var connector = modelTo.dataSource.connector;
var pk = this.definition.keyTo;
var pkProp = modelTo.definition.properties[pk];
var pkType = pkProp && pkProp.type;
var embeddedList = this.embeddedList();
targetModelData = targetModelData || {};
var assignId = (forceId || targetModelData[pk] === undefined);
assignId = assignId && !persistent;
if (assignId && pkType === Number) {
var ids = embeddedList.map(function(m) {
return (typeof m[pk] === 'number' ? m[pk] : 0);
});
if (ids.length > 0) {
targetModelData[pk] = Math.max.apply(null, ids) + 1;
} else {
targetModelData[pk] = 1;
}
} else if (assignId && typeof connector.generateId === 'function') {
var id = connector.generateId(modelTo.modelName, targetModelData, pk);
targetModelData[pk] = id;
}
this.definition.applyProperties(modelInstance, targetModelData);
var inst = new modelTo(targetModelData);
if (this.definition.options.prepend) {
embeddedList.unshift(inst);
} else {
embeddedList.push(inst);
}
this.prepareEmbeddedInstance(inst);
return inst;
};
/**
* Add the target model instance to the 'embedsMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
EmbedsMany.prototype.add = function(acInst, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.emails.add(acInst, data, cb)
cb = options;
options = {};
} else if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// customer.emails.add(acInst, cb)
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
var self = this;
var definition = this.definition;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var defOpts = definition.options;
var belongsTo = defOpts.belongsTo && modelTo.relations[defOpts.belongsTo];
if (!belongsTo) {
throw new Error('Invalid reference: ' + defOpts.belongsTo || '(none)');
}
var fk2 = belongsTo.keyTo;
var pk2 = belongsTo.modelTo.definition.idName() || 'id';
var query = {};
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
var filter = {where: query};
belongsTo.applyScope(modelInstance, filter);
belongsTo.modelTo.findOne(filter, options, function(err, ref) {
if (ref instanceof belongsTo.modelTo) {
var inst = self.build(data || {});
inst[defOpts.belongsTo](ref);
modelInstance.save(function(err) {
cb(err, err ? null : inst);
});
} else {
cb(null, null);
}
});
return cb.promise;
};
/**
* Remove the target model instance from the 'embedsMany' relation
* @param {Object|ID) acInst The actual instance or id value
*/
EmbedsMany.prototype.remove = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.emails.remove(acInst, cb)
cb = options;
options = {};
}
var self = this;
var definition = this.definition;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var defOpts = definition.options;
var belongsTo = defOpts.belongsTo && modelTo.relations[defOpts.belongsTo];
if (!belongsTo) {
throw new Error('Invalid reference: ' + defOpts.belongsTo || '(none)');
}
var fk2 = belongsTo.keyTo;
var pk2 = belongsTo.modelTo.definition.idName() || 'id';
var query = {};
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
var filter = {where: query};
belongsTo.applyScope(modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelInstance[definition.name](filter, options, function(err, items) {
if (err) return cb(err);
items.forEach(function(item) {
self.unset(item);
});
modelInstance.save(options, function(err) {
cb(err);
});
});
return cb.promise;
};
RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, params) {
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
var thisClassName = modelFrom.modelName;
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
var fk = params.foreignKey || i8n.camelize(modelTo.modelName + '_ids', true);
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
var idType = modelTo.definition.properties[idName].type;
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.referencesMany,
modelFrom: modelFrom,
keyFrom: fk,
keyTo: idName,
modelTo: modelTo,
multiple: true,
properties: params.properties,
scope: params.scope,
options: params.options,
});
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {
type: [idType], default: function() { return []; },
});
modelFrom.validate(relationName, function(err) {
var ids = this[fk] || [];
var uniqueIds = ids.filter(function(id, pos) {
return utils.findIndexOf(ids, id, idEquals) === pos;
});
if (ids.length !== uniqueIds.length) {
var msg = 'contains duplicate `' + modelTo.modelName + '` instance';
this.errors.add(relationName, msg, 'uniqueness');
err(false);
}
}, {code: 'uniqueness'});
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists'),
add: scopeMethod(definition, 'add'),
remove: scopeMethod(definition, 'remove'),
at: scopeMethod(definition, 'at'),
};
var findByIdFunc = scopeMethods.findById;
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
var destroyByIdFunc = scopeMethods.destroy;
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
var updateByIdFunc = scopeMethods.updateById;
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
var addFunc = scopeMethods.add;
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
scopeMethods.related = scopeMethod(definition, 'related');
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
for (var i = 0; i < customMethods.length; i++) {
var methodName = customMethods[i];
var method = scopeMethods[methodName];
if (typeof method === 'function' && method.shared === true) {
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
}
};
// Mix the property and scoped methods into the prototype class
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
return {};
}, scopeMethods, definition.options);
scopeDefinition.related = scopeMethods.related; // bound to definition
return definition;
};
ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
var fk = this.definition.keyFrom;
var modelTo = this.definition.modelTo;
var relationName = this.definition.name;
var modelInstance = this.modelInstance;
var self = receiver;
var actualCond = {};
var actualRefresh = false;
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// customer.orders(receiver, scopeParams, cb)
cb = condOrRefresh;
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
// customer.orders(receiver, scopeParams, condOrRefresh, cb)
cb = options;
options = {};
if (typeof condOrRefresh === 'boolean') {
actualRefresh = condOrRefresh;
condOrRefresh = {};
} else {
actualRefresh = true;
}
}
actualCond = condOrRefresh || {};
var ids = self[fk] || [];
this.definition.applyScope(modelInstance, actualCond);
var params = mergeQuery(actualCond, scopeParams);
return modelTo.findByIds(ids, params, options, cb);
};
ReferencesMany.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.findById(fkId, cb)
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var modelFrom = this.definition.modelFrom;
var relationName = this.definition.name;
var modelInstance = this.modelInstance;
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
if (typeof fkId === 'object') {
fkId = fkId.toString(); // mongodb
}
var ids = modelInstance[fk] || [];
var filter = {};
this.definition.applyScope(modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelTo.findByIds([fkId], filter, options, function(err, instances) {
if (err) {
return cb(err);
}
var inst = instances[0];
if (!inst) {
err = new Error(g.f('No instance with {{id}} %s found for %s', fkId, modelTo.modelName));
err.statusCode = 404;
return cb(err);
}
// Check if the foreign key is amongst the ids
if (utils.findIndexOf(ids, inst[pk], idEquals) > -1) {
cb(null, inst);
} else {
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
modelFrom.modelName, fk, modelInstance[fk],
modelTo.modelName, pk, inst[pk]));
err.statusCode = 400;
cb(err);
}
});
return cb.promise;
};
ReferencesMany.prototype.exists = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.exists(fkId, cb)
cb = options;
options = {};
}
var fk = this.definition.keyFrom;
var ids = this.modelInstance[fk] || [];
cb = cb || utils.createPromiseCallback();
process.nextTick(function() { cb(null, utils.findIndexOf(ids, fkId, idEquals) > -1); });
return cb.promise;
};
ReferencesMany.prototype.updateById = function(fkId, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.updateById(fkId, data, cb)
cb = options;
options = {};
} else if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// customer.orders.updateById(fkId, cb)
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
this.findById(fkId, options, function(err, inst) {
if (err) return cb(err);
inst.updateAttributes(data, options, cb);
});
return cb.promise;
};
ReferencesMany.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.destroyById(fkId, cb)
cb = options;
options = {};
}
var self = this;
cb = cb || utils.createPromiseCallback();
this.findById(fkId, function(err, inst) {
if (err) return cb(err);
self.remove(inst, function(err, ids) {
inst.destroy(cb);
});
});
return cb.promise;
};
ReferencesMany.prototype.at = function(index, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.at(index, cb)
cb = options;
options = {};
}
var fk = this.definition.keyFrom;
var ids = this.modelInstance[fk] || [];
cb = cb || utils.createPromiseCallback();
this.findById(ids[index], options, cb);
return cb.promise;
};
ReferencesMany.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.create(data, cb)
cb = options;
options = {};
}
var definition = this.definition;
var modelTo = this.definition.modelTo;
var relationName = this.definition.name;
var modelInstance = this.modelInstance;
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var ids = modelInstance[fk] || [];
var inst = this.callScopeMethod('build', targetModelData);
inst.save(options, function(err, inst) {
if (err) return cb(err, inst);
var id = inst[pk];
if (typeof id === 'object') {
id = id.toString(); // mongodb
}
if (definition.options.prepend) {
ids.unshift(id);
} else {
ids.push(id);
}
modelInstance.updateAttribute(fk,
ids, options, function(err, modelInst) {
cb(err, inst);
});
});
return cb.promise;
};
ReferencesMany.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
targetModelData = targetModelData || {};
this.definition.applyProperties(this.modelInstance, targetModelData);
return new modelTo(targetModelData);
};
/**
* Add the target model instance to the 'embedsMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
ReferencesMany.prototype.add = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.add(acInst, cb)
cb = options;
options = {};
}
var self = this;
var definition = this.definition;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var insert = function(inst, done) {
var id = inst[pk];
if (typeof id === 'object') {
id = id.toString(); // mongodb
}
var ids = modelInstance[fk] || [];
if (definition.options.prepend) {
ids.unshift(id);
} else {
ids.push(id);
}
modelInstance.updateAttribute(fk, ids, options, function(err) {
done(err, err ? null : inst);
});
};
cb = cb || utils.createPromiseCallback();
if (acInst instanceof modelTo) {
insert(acInst, cb);
} else {
var filter = {where: {}};
filter.where[pk] = acInst;
definition.applyScope(modelInstance, filter);
modelTo.findOne(filter, options, function(err, inst) {
if (err || !inst) return cb(err, null);
insert(inst, cb);
});
}
return cb.promise;
};
/**
* Remove the target model instance from the 'embedsMany' relation
* @param {Object|ID) acInst The actual instance or id value
*/
ReferencesMany.prototype.remove = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
// customer.orders.remove(acInst, cb)
cb = options;
options = {};
}
var definition = this.definition;
var modelInstance = this.modelInstance;
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var ids = modelInstance[fk] || [];
var id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst;
cb = cb || utils.createPromiseCallback();
var index = utils.findIndexOf(ids, id, idEquals);
if (index > -1) {
ids.splice(index, 1);
modelInstance.updateAttribute(fk, ids, options, function(err, inst) {
cb(err, inst[fk] || []);
});
} else {
process.nextTick(function() { cb(null, ids); });
}
return cb.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/*!
* Dependencies
*/
var relation = require('./relation-definition');
var RelationDefinition = relation.RelationDefinition;
module.exports = RelationMixin;
/**
* RelationMixin class. Use to define relationships between models.
*
* @class RelationMixin
*/
function RelationMixin() {
}
/**
* Define a "one to many" relationship by specifying the model name
*
* Examples:
* ```
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
* ```
*
* ```
* Book.hasMany(Chapter);
* ```
* Or, equivalently:
* ```
* Book.hasMany('chapters', {model: Chapter});
* ```
*
* Query and create related models:
*
* ```js
* Book.create(function(err, book) {
*
* // Create a chapter instance ready to be saved in the data source.
* var chapter = book.chapters.build({name: 'Chapter 1'});
*
* // Save the new chapter
* chapter.save();
*
* // you can also call the Chapter.create method with the `chapters` property which will build a chapter
* // instance and save the it in the data source.
* book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
* // this callback is optional
* });
*
* // Query chapters for the book
* book.chapters(function(err, chapters) { // all chapters with bookId = book.id
* console.log(chapters);
* });
*
* book.chapters({where: {name: 'test'}, function(err, chapters) {
* // All chapters with bookId = book.id and name = 'test'
* console.log(chapters);
* });
* });
*```
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
* @options {Object} parameters Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationMixin.hasMany = function hasMany(modelTo, params) {
return RelationDefinition.hasMany(this, modelTo, params);
};
/**
* Declare "belongsTo" relation that sets up a one-to-one connection with another model, such that each
* instance of the declaring model "belongs to" one instance of the other model.
*
* For example, if an application includes users and posts, and each post can be written by exactly one user.
* The following code specifies that `Post` has a reference called `author` to the `User` model via the `userId` property of `Post`
* as the foreign key.
* ```
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
* ```
* You can then access the author in one of the following styles.
* Get the User object for the post author asynchronously:
* ```
* post.author(callback);
* ```
* Get the User object for the post author synchronously:
* ```
* post.author();
* ```
* Set the author to be the given user:
* ```
* post.author(user)
* ```
* Examples:
*
* Suppose the model Post has a *belongsTo* relationship with User (the author of the post). You could declare it this way:
* ```js
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
* ```
*
* When a post is loaded, you can load the related author with:
* ```js
* post.author(function(err, user) {
* // the user variable is your user object
* });
* ```
*
* The related object is cached, so if later you try to get again the author, no additional request will be made.
* But there is an optional boolean parameter in first position that set whether or not you want to reload the cache:
* ```js
* post.author(true, function(err, user) {
* // The user is reloaded, even if it was already cached.
* });
* ```
* This optional parameter default value is false, so the related object will be loaded from cache if available.
*
* @param {Class|String} modelTo Model object (or String name of model) to which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
* @property {String} foreignKey Name of foreign key property.
*
*/
RelationMixin.belongsTo = function(modelTo, params) {
return RelationDefinition.belongsTo(this, modelTo, params);
};
/**
* A hasAndBelongsToMany relation creates a direct many-to-many connection with another model, with no intervening model.
* For example, if your application includes users and groups, with each group having many users and each user appearing
* in many groups, you could declare the models this way:
* ```
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
* ```
* Then, to get the groups to which the user belongs:
* ```
* user.groups(callback);
* ```
* Create a new group and connect it with the user:
* ```
* user.groups.create(data, callback);
* ```
* Connect an existing group with the user:
* ```
* user.groups.add(group, callback);
* ```
* Remove the user from the group:
* ```
* user.groups.remove(group, callback);
* ```
*
* @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship.
* the relation
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) {
return RelationDefinition.hasAndBelongsToMany(this, modelTo, params);
};
RelationMixin.hasOne = function hasOne(modelTo, params) {
return RelationDefinition.hasOne(this, modelTo, params);
};
RelationMixin.referencesMany = function referencesMany(modelTo, params) {
return RelationDefinition.referencesMany(this, modelTo, params);
};
RelationMixin.embedsOne = function embedsOne(modelTo, params) {
return RelationDefinition.embedsOne(this, modelTo, params);
};
RelationMixin.embedsMany = function embedsMany(modelTo, params) {
return RelationDefinition.embedsMany(this, modelTo, params);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var i8n = require('inflection');
var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations;
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var mergeQuery = utils.mergeQuery;
var DefaultModelBaseClass = require('./model.js');
var collectTargetIds = utils.collectTargetIds;
var idName = utils.idName;
/**
* Module exports
*/
exports.defineScope = defineScope;
function ScopeDefinition(definition) {
this.isStatic = definition.isStatic;
this.modelFrom = definition.modelFrom;
this.modelTo = definition.modelTo || definition.modelFrom;
this.name = definition.name;
this.params = definition.params;
this.methods = definition.methods || {};
this.options = definition.options || {};
}
ScopeDefinition.prototype.targetModel = function(receiver) {
var modelTo;
if (typeof this.options.modelTo === 'function') {
modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
} else {
modelTo = this.modelTo;
}
if (!(modelTo.prototype instanceof DefaultModelBaseClass)) {
var msg = 'Invalid target model for scope `';
msg += (this.isStatic ? this.modelFrom : this.modelFrom.constructor).modelName;
msg += this.isStatic ? '.' : '.prototype.';
msg += this.name + '`.';
throw new Error(msg);
}
return modelTo;
};
/*!
* Find related model instances
* @param {*} receiver The target model class/prototype
* @param {Object|Function} scopeParams
* @param {Boolean|Object} [condOrRefresh] true for refresh or object as a filter
* @param {Object} [options]
* @param {Function} cb
* @returns {*}
*/
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
var name = this.name;
var self = receiver;
var actualCond = {};
var actualRefresh = false;
var saveOnCache = receiver instanceof DefaultModelBaseClass;
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// related(receiver, scopeParams, cb)
cb = condOrRefresh;
options = {};
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
options = options || {};
if (condOrRefresh !== undefined) {
if (typeof condOrRefresh === 'boolean') {
actualRefresh = condOrRefresh;
} else {
actualCond = condOrRefresh;
actualRefresh = true;
saveOnCache = false;
}
}
cb = cb || utils.createPromiseCallback();
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined ||
actualRefresh) {
// It either doesn't hit the cache or refresh is required
var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
var targetModel = this.targetModel(receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/166
var scopeOnRelatedModel = params.collect &&
params.include.scope !== null &&
typeof params.include.scope === 'object';
if (scopeOnRelatedModel) {
var filter = params.include;
// The filter applied on relatedModel
var queryRelated = filter.scope;
delete params.include.scope;
};
targetModel.find(params, options, function(err, data) {
if (!err && saveOnCache) {
defineCachedRelations(self);
self.__cachedRelations[name] = data;
}
if (scopeOnRelatedModel === true) {
var relatedModel = targetModel.relations[filter.relation].modelTo;
var IdKey = idName(relatedModel);
// Merge queryRelated filter and targetId filter
var buildWhere = function() {
var IdKeyCondition = {};
IdKeyCondition[IdKey] = collectTargetIds(data, IdKey);
var mergedWhere = {
and: [IdKeyCondition, queryRelated.where],
};
return mergedWhere;
};
if (queryRelated.where !== undefined) {
queryRelated.where = buildWhere();
} else {
queryRelated.where = {};
queryRelated.where[IdKey] = collectTargetIds(data, IdKey);
}
relatedModel.find(queryRelated, cb);
} else {
cb(err, data);
}
});
} else {
// Return from cache
cb(null, self.__cachedRelations[name]);
}
return cb.promise;
};
/**
* Define a scope method
* @param {String} name of the method
* @param {Function} function to define
*/
ScopeDefinition.prototype.defineMethod = function(name, fn) {
return this.methods[name] = fn;
};
/**
* Define a scope to the class
* @param {Model} cls The class where the scope method is added
* @param {Model} targetClass The class that a query to run against
* @param {String} name The name of the scope
* @param {Object|Function} params The parameters object for the query or a function
* to return the query object
* @param methods An object of methods keyed by the method name to be bound to the class
*/
function defineScope(cls, targetClass, name, params, methods, options) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
}
// only makes sense to add scope in meta if base and target classes
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
} else if (targetClass) {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
}
}
options = options || {};
// Check if the cls is the class itself or its prototype
var isStatic = (typeof cls === 'function') || options.isStatic || false;
var definition = new ScopeDefinition({
isStatic: isStatic,
modelFrom: cls,
modelTo: targetClass,
name: name,
params: params,
methods: methods,
options: options,
});
if (isStatic) {
cls.scopes = cls.scopes || {};
cls.scopes[name] = definition;
} else {
cls.constructor.scopes = cls.constructor.scopes || {};
cls.constructor.scopes[name] = definition;
}
// Define a property for the scope
Object.defineProperty(cls, name, {
enumerable: false,
configurable: true,
/**
* This defines a property for the scope. For example, user.accounts or
* User.vips. Please note the cls can be the model class or prototype of
* the model class.
*
* The property value is function. It can be used to query the scope,
* such as user.accounts(condOrRefresh, cb) or User.vips(cb). The value
* can also have child properties for create/build/delete. For example,
* user.accounts.create(act, cb).
*
*/
get: function() {
var targetModel = definition.targetModel(this);
var self = this;
var f = function(condOrRefresh, options, cb) {
if (arguments.length === 0) {
if (typeof f.value === 'function') {
return f.value(self);
} else if (self.__cachedRelations) {
return self.__cachedRelations[name];
}
} else {
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// customer.orders(cb)
cb = condOrRefresh;
options = {};
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
// customer.orders(condOrRefresh, cb);
cb = options;
options = {};
}
options = options || {};
// Check if there is a through model
// see https://github.com/strongloop/loopback/issues/1076
if (f._scope.collect &&
condOrRefresh !== null && typeof condOrRefresh === 'object') {
f._scope.include = {
relation: f._scope.collect,
scope: condOrRefresh,
};
condOrRefresh = {};
}
return definition.related(self, f._scope, condOrRefresh, options, cb);
}
};
f._receiver = this;
f._scope = typeof definition.params === 'function' ?
definition.params.call(self) : definition.params;
f._targetClass = targetModel.modelName;
if (f._scope.collect) {
const rel = targetModel.relations[f._scope.collect];
f._targetClass = rel && rel.modelTo && rel.modelTo.modelName || i8n.camelize(f._scope.collect);
}
f.getAsync = function(condOrRefresh, options, cb) {
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// customer.orders.getAsync(cb)
cb = condOrRefresh;
options = {};
condOrRefresh = {};
} else if (typeof options === 'function' && cb === undefined) {
// customer.orders.getAsync(condOrRefresh, cb);
cb = options;
options = {};
}
options = options || {};
return definition.related(self, f._scope, condOrRefresh, options, cb);
};
f.build = build;
f.create = create;
f.updateAll = updateAll;
f.destroyAll = destroyAll;
f.findById = findById;
f.findOne = findOne;
f.count = count;
for (var i in definition.methods) {
f[i] = definition.methods[i].bind(self);
}
if (!targetClass) return f;
// Define scope-chaining, such as
// Station.scope('active', {where: {isActive: true}});
// Station.scope('subway', {where: {isUndeground: true}});
// Station.active.subway(cb);
Object.keys(targetClass._scopeMeta).forEach(function(name) {
Object.defineProperty(f, name, {
enumerable: false,
get: function() {
mergeQuery(f._scope, targetModel._scopeMeta[name]);
return f;
},
});
}.bind(self));
return f;
},
});
// Wrap the property into a function for remoting
var fn = function() {
// primaryObject.scopeName, such as user.accounts
var f = this[name];
// set receiver to be the scope property whose value is a function
f.apply(this[name], arguments);
};
cls['__get__' + name] = fn;
var fnCreate = function() {
var f = this[name].create;
f.apply(this[name], arguments);
};
cls['__create__' + name] = fnCreate;
var fnDelete = function() {
var f = this[name].destroyAll;
f.apply(this[name], arguments);
};
cls['__delete__' + name] = fnDelete;
var fnUpdate = function() {
var f = this[name].updateAll;
f.apply(this[name], arguments);
};
cls['__update__' + name] = fnUpdate;
var fnFindById = function(cb) {
var f = this[name].findById;
f.apply(this[name], arguments);
};
cls['__findById__' + name] = fnFindById;
var fnFindOne = function(cb) {
var f = this[name].findOne;
f.apply(this[name], arguments);
};
cls['__findOne__' + name] = fnFindOne;
var fnCount = function(cb) {
var f = this[name].count;
f.apply(this[name], arguments);
};
cls['__count__' + name] = fnCount;
// and it should have create/build methods with binded thisModelNameId param
function build(data) {
data = data || {};
// Find all fixed property values for the scope
var targetModel = definition.targetModel(this._receiver);
var where = (this._scope && this._scope.where) || {};
setScopeValuesFromWhere(data, where, targetModel);
return new targetModel(data);
}
function create(data, options, cb) {
if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// create(cb)
cb = data;
data = {};
} else if (typeof options === 'function' && cb === undefined) {
// create(data, cb)
cb = options;
options = {};
}
options = options || {};
return this.build(data).save(options, cb);
}
/*
Callback
- The callback will be called after all elements are destroyed
- For every destroy call which results in an error
- If fetching the Elements on which destroyAll is called results in an error
*/
function destroyAll(where, options, cb) {
if (typeof where === 'function') {
// destroyAll(cb)
cb = where;
where = {};
} else if (typeof options === 'function' && cb === undefined) {
// destroyAll(where, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.destroyAll(filter.where, options, cb);
}
function updateAll(where, data, options, cb) {
if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// updateAll(data, cb)
cb = data;
data = where;
where = {};
options = {};
} else if (typeof options === 'function' && cb === undefined) {
// updateAll(where, data, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.updateAll(filter.where, data, options, cb);
}
function findById(id, filter, options, cb) {
if (options === undefined && cb === undefined) {
if (typeof filter === 'function') {
// findById(id, cb)
cb = filter;
filter = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// findById(id, query, cb)
cb = options;
options = {};
if (typeof filter === 'object' && !(filter.include || filter.fields)) {
// If filter doesn't have include or fields, assuming it's options
options = filter;
filter = {};
}
}
}
options = options || {};
filter = filter || {};
var targetModel = definition.targetModel(this._receiver);
var idName = targetModel.definition.idName();
var query = {where: {}};
query.where[idName] = id;
query = mergeQuery(query, filter);
return this.findOne(query, options, cb);
}
function findOne(filter, options, cb) {
if (typeof filter === 'function') {
// findOne(cb)
cb = filter;
filter = {};
options = {};
} else if (typeof options === 'function' && cb === undefined) {
// findOne(filter, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
filter = mergeQuery({where: scoped}, filter || {});
return targetModel.findOne(filter, options, cb);
}
function count(where, options, cb) {
if (typeof where === 'function') {
// count(cb)
cb = where;
where = {};
} else if (typeof options === 'function' && cb === undefined) {
// count(where, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.count(filter.where, options, cb);
}
return definition;
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('strong-globalize')();
var debug = require('debug')('loopback:connector:transaction');
var uuid = require('uuid');
var utils = require('./utils');
var jutil = require('./jutil');
var ObserverMixin = require('./observer');
var Transaction = require('loopback-connector').Transaction;
module.exports = TransactionMixin;
/**
* TransactionMixin class. Use to add transaction APIs to a model class.
*
* @class TransactionMixin
*/
function TransactionMixin() {
}
/**
* Begin a new transaction
* @param {Object|String} [options] Options can be one of the forms:
* - Object: {isolationLevel: '...', timeout: 1000}
* - String: isolationLevel
*
* Valid values of `isolationLevel` are:
*
* - Transaction.READ_COMMITTED = 'READ COMMITTED'; // default
* - Transaction.READ_UNCOMMITTED = 'READ UNCOMMITTED';
* - Transaction.SERIALIZABLE = 'SERIALIZABLE';
* - Transaction.REPEATABLE_READ = 'REPEATABLE READ';
*
* @param {Function} cb Callback function. It calls back with (err, transaction).
* To pass the transaction context to one of the CRUD methods, use the `options`
* argument with `transaction` property, for example,
*
* ```js
*
* MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
* MyModel.create({x: 1, y: 'a'}, {transaction: tx}, function(err, inst) {
* MyModel.find({x: 1}, {transaction: tx}, function(err, results) {
* // ...
* tx.commit(function(err) {...});
* });
* });
* });
* ```
*
* The transaction can be committed or rolled back. If timeout happens, the
* transaction will be rolled back. Please note a transaction is typically
* associated with a pooled connection. Committing or rolling back a transaction
* will release the connection back to the pool.
*
* Once the transaction is committed or rolled back, the connection property
* will be set to null to mark the transaction to be inactive. Trying to commit
* or rollback an inactive transaction will receive an error from the callback.
*
* Please also note that the transaction is only honored with the same data
* source/connector instance. CRUD methods will not join the current transaction
* if its model is not attached the same data source.
*
*/
TransactionMixin.beginTransaction = function(options, cb) {
cb = cb || utils.createPromiseCallback();
if (Transaction) {
var connector = this.getConnector();
Transaction.begin(connector, options, function(err, transaction) {
if (err) return cb(err);
if (transaction) {
// Set an informational transaction id
transaction.id = uuid.v1();
}
if (options.timeout) {
setTimeout(function() {
var context = {
transaction: transaction,
operation: 'timeout',
};
transaction.notifyObserversOf('timeout', context, function(err) {
if (!err) {
transaction.rollback(function() {
debug('Transaction %s is rolled back due to timeout',
transaction.id);
});
}
});
}, options.timeout);
}
cb(err, transaction);
});
} else {
process.nextTick(function() {
var err = new Error(g.f('{{Transaction}} is not supported'));
cb(err);
});
}
return cb.promise;
};
// Promisify the transaction apis
Eif (Transaction) {
jutil.mixin(Transaction.prototype, ObserverMixin);
/**
* Commit a transaction and release it back to the pool
* @param {Function} cb Callback function
* @returns {Promise|undefined}
*/
Transaction.prototype.commit = function(cb) {
var self = this;
cb = cb || utils.createPromiseCallback();
// Report an error if the transaction is not active
if (!self.connection) {
process.nextTick(function() {
cb(new Error(g.f('The {{transaction}} is not active: %s', self.id)));
});
return cb.promise;
}
var context = {
transaction: self,
operation: 'commit',
};
function work(done) {
self.connector.commit(self.connection, done);
}
self.notifyObserversAround('commit', context, work, function(err) {
// Deference the connection to mark the transaction is not active
// The connection should have been released back the pool
self.connection = null;
cb(err);
});
return cb.promise;
};
/**
* Rollback a transaction and release it back to the pool
* @param {Function} cb Callback function
* @returns {Promise|undefined}
*/
Transaction.prototype.rollback = function(cb) {
var self = this;
cb = cb || utils.createPromiseCallback();
// Report an error if the transaction is not active
if (!self.connection) {
process.nextTick(function() {
cb(new Error(g.f('The {{transaction}} is not active: %s', self.id)));
});
return cb.promise;
}
var context = {
transaction: self,
operation: 'rollback',
};
function work(done) {
self.connector.rollback(self.connection, done);
}
self.notifyObserversAround('rollback', context, work, function(err) {
// Deference the connection to mark the transaction is not active
// The connection should have been released back the pool
self.connection = null;
cb(err);
});
return cb.promise;
};
Transaction.prototype.toJSON = function() {
return this.id;
};
Transaction.prototype.toString = function() {
return this.id;
};
}
TransactionMixin.Transaction = Transaction;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 1 1 1 1 1 1 1 1 2 2 6 2 2 22 22 22 24 2 2 2 2 2 2 2 2 2 2 2 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var Types = {};
/**
* Schema types
*/
Types.Text = function Text(value) {
if (!(this instanceof Text)) {
return value;
}
this.value = value;
}; // Text type
Types.Text.prototype.toObject = Types.Text.prototype.toJSON = function() {
return this.value;
};
Types.JSON = function JSON(value) {
if (!(this instanceof JSON)) {
return value;
}
this.value = value;
}; // JSON Object
Types.JSON.prototype.toObject = Types.JSON.prototype.toJSON = function() {
return this.value;
};
Types.Any = function Any(value) {
if (!(this instanceof Any)) {
return value;
}
this.value = value;
}; // Any Type
Types.Any.prototype.toObject = Types.Any.prototype.toJSON = function() {
return this.value;
};
module.exports = function(modelTypes) {
var GeoPoint = require('./geo').GeoPoint;
for (var t in Types) {
modelTypes[t] = Types[t];
}
modelTypes.schemaTypes = {};
modelTypes.registerType = function(type, names) {
names = names || [];
names = names.concat([type.name]);
for (var n = 0; n < names.length; n++) {
this.schemaTypes[names[n].toLowerCase()] = type;
}
};
modelTypes.registerType(Types.Text);
modelTypes.registerType(Types.JSON);
modelTypes.registerType(Types.Any);
modelTypes.registerType(String);
modelTypes.registerType(Number);
modelTypes.registerType(Boolean);
modelTypes.registerType(Date);
modelTypes.registerType(Buffer, ['Binary']);
modelTypes.registerType(Array);
modelTypes.registerType(GeoPoint);
modelTypes.registerType(Object);
};
module.exports.Types = Types;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 19 19 19 19 12 30 19 19 34 34 26 8 8 19 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2012,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
exports.safeRequire = safeRequire;
exports.fieldsToArray = fieldsToArray;
exports.selectFields = selectFields;
exports.removeUndefined = removeUndefined;
exports.parseSettings = parseSettings;
exports.mergeSettings = exports.deepMerge = mergeSettings;
exports.isPlainObject = isPlainObject;
exports.defineCachedRelations = defineCachedRelations;
exports.sortObjectsByIds = sortObjectsByIds;
exports.setScopeValuesFromWhere = setScopeValuesFromWhere;
exports.mergeQuery = mergeQuery;
exports.mergeIncludes = mergeIncludes;
exports.createPromiseCallback = createPromiseCallback;
exports.uniq = uniq;
exports.toRegExp = toRegExp;
exports.hasRegExpFlags = hasRegExpFlags;
exports.idEquals = idEquals;
exports.findIndexOf = findIndexOf;
exports.collectTargetIds = collectTargetIds;
exports.idName = idName;
var g = require('strong-globalize')();
var traverse = require('traverse');
var assert = require('assert');
var Promise = require('bluebird');
function safeRequire(module) {
try {
return require(module);
} catch (e) {
g.log('Run "{{npm install loopback-datasource-juggler}} %s" command ',
'to use {{loopback-datasource-juggler}} using %s database engine',
module, module);
process.exit(1);
}
}
/*
* Extracting fixed property values for the scope from the where clause into
* the data object
*
* @param {Object} The data object
* @param {Object} The where clause
*/
function setScopeValuesFromWhere(data, where, targetModel) {
for (var i in where) {
if (i === 'and') {
// Find fixed property values from each subclauses
for (var w = 0, n = where[i].length; w < n; w++) {
setScopeValuesFromWhere(data, where[i][w], targetModel);
}
continue;
}
var prop = targetModel.definition.properties[i];
if (prop) {
var val = where[i];
if (typeof val !== 'object' || val instanceof prop.type ||
prop.type.name === 'ObjectID') { // MongoDB key
// Only pick the {propertyName: propertyValue}
data[i] = where[i];
}
}
}
}
/**
* Merge include options of default scope with runtime include option.
* exhibits the _.extend behaviour. Property value of source overrides
* property value of destination if property name collision occurs
* @param {String|Array|Object} destination The default value of `include` option
* @param {String|Array|Object} source The runtime value of `include` option
* @returns {Object}
*/
function mergeIncludes(destination, source) {
var destArray = convertToArray(destination);
var sourceArray = convertToArray(source);
if (destArray.length === 0) {
return sourceArray;
}
if (sourceArray.length === 0) {
return destArray;
}
var relationNames = [];
var resultArray = [];
for (var j in sourceArray) {
var sourceEntry = sourceArray[j];
var sourceEntryRelationName = (typeof (sourceEntry.rel || sourceEntry.relation) === 'string') ?
sourceEntry.relation : Object.keys(sourceEntry)[0];
relationNames.push(sourceEntryRelationName);
resultArray.push(sourceEntry);
}
for (var i in destArray) {
var destEntry = destArray[i];
var destEntryRelationName = (typeof (destEntry.rel || destEntry.relation) === 'string') ?
destEntry.relation : Object.keys(destEntry)[0];
if (relationNames.indexOf(destEntryRelationName) === -1) {
resultArray.push(destEntry);
}
}
return resultArray;
}
/**
* Converts input parameter into array of objects which wraps the value.
* "someValue" is converted to [{"someValue":true}]
* ["someValue"] is converted to [{"someValue":true}]
* {"someValue":true} is converted to [{"someValue":true}]
* @param {String|Array|Object} param - Input parameter to be converted
* @returns {Array}
*/
function convertToArray(include) {
if (typeof include === 'string') {
const obj = {};
obj[include] = true;
return [obj];
} else if (isPlainObject(include)) {
// if include is of the form - {relation:'',scope:''}
if (include.rel || include.relation) {
return [include];
}
// Build an array of key/value pairs
var newInclude = [];
for (var key in include) {
const obj = {};
obj[key] = include[key];
newInclude.push(obj);
}
return newInclude;
} else if (Array.isArray(include)) {
var normalized = [];
for (var i in include) {
var includeEntry = include[i];
if (typeof includeEntry === 'string') {
const obj = {};
obj[includeEntry] = true;
normalized.push(obj);
} else {
normalized.push(includeEntry);
}
}
return normalized;
}
return [];
}
/*!
* Merge query parameters
* @param {Object} base The base object to contain the merged results
* @param {Object} update The object containing updates to be merged
* @param {Object} spec Optionally specifies parameters to exclude (set to false)
* @returns {*|Object} The base object
* @private
*/
function mergeQuery(base, update, spec) {
if (!update) {
return;
}
spec = spec || {};
base = base || {};
if (update.where && Object.keys(update.where).length > 0) {
if (base.where && Object.keys(base.where).length > 0) {
base.where = {and: [base.where, update.where]};
} else {
base.where = update.where;
}
}
// Merge inclusion
if (spec.include !== false && update.include) {
if (!base.include) {
base.include = update.include;
} else {
if (spec.nestedInclude === true) {
// specify nestedInclude=true to force nesting of inclusions on scoped
// queries. e.g. In physician.patients.getAsync({include: 'address'}),
// inclusion should be on patient model, not on physician model.
var saved = base.include;
base.include = {};
base.include[update.include] = saved;
} else {
// default behaviour of inclusion merge - merge inclusions at the same
// level. - https://github.com/strongloop/loopback-datasource-juggler/pull/569#issuecomment-95310874
base.include = mergeIncludes(base.include, update.include);
}
}
}
if (spec.collect !== false && update.collect) {
base.collect = update.collect;
}
// Overwrite fields
if (spec.fields !== false && update.fields !== undefined) {
base.fields = update.fields;
} else if (update.fields !== undefined) {
base.fields = [].concat(base.fields).concat(update.fields);
}
// set order
if ((!base.order || spec.order === false) && update.order) {
base.order = update.order;
}
// overwrite pagination
if (spec.limit !== false && update.limit !== undefined) {
base.limit = update.limit;
}
var skip = spec.skip !== false && spec.offset !== false;
if (skip && update.skip !== undefined) {
base.skip = update.skip;
}
if (skip && update.offset !== undefined) {
base.offset = update.offset;
}
return base;
}
/**
* Normalize fields to an array of included properties
* @param {String|String[]|Object} fields Fields filter
* @param {String[]} properties Property names
* @param {Boolean} excludeUnknown To exclude fields that are unknown properties
* @returns {String[]} An array of included property names
*/
function fieldsToArray(fields, properties, excludeUnknown) {
if (!fields) return;
// include all properties by default
var result = properties;
var i, n;
if (typeof fields === 'string') {
result = [fields];
} else if (Array.isArray(fields) && fields.length > 0) {
// No empty array, including all the fields
result = fields;
} else if ('object' === typeof fields) {
// { field1: boolean, field2: boolean ... }
var included = [];
var excluded = [];
var keys = Object.keys(fields);
if (!keys.length) return;
for (i = 0, n = keys.length; i < n; i++) {
var k = keys[i];
if (fields[k]) {
included.push(k);
} else if ((k in fields) && !fields[k]) {
excluded.push(k);
}
}
if (included.length > 0) {
result = included;
} else if (excluded.length > 0) {
for (i = 0, n = excluded.length; i < n; i++) {
var index = result.indexOf(excluded[i]);
if (index !== -1) result.splice(index, 1); // only when existing field excluded
}
}
}
var fieldArray = [];
if (excludeUnknown) {
for (i = 0, n = result.length; i < n; i++) {
if (properties.indexOf(result[i]) !== -1) {
fieldArray.push(result[i]);
}
}
} else {
fieldArray = result;
}
return fieldArray;
}
function selectFields(fields) {
// map function
return function(obj) {
var result = {};
var key;
for (var i = 0; i < fields.length; i++) {
key = fields[i];
result[key] = obj[key];
}
return result;
};
}
/**
* Remove undefined values from the queury object
* @param query
* @param handleUndefined {String} either "nullify", "throw" or "ignore" (default: "ignore")
* @returns {exports.map|*}
*/
function removeUndefined(query, handleUndefined) {
if (typeof query !== 'object' || query === null) {
return query;
}
// WARNING: [rfeng] Use map() will cause mongodb to produce invalid BSON
// as traverse doesn't transform the ObjectId correctly
return traverse(query).forEach(function(x) {
if (x === undefined) {
switch (handleUndefined) {
case 'nullify':
this.update(null);
break;
case 'throw':
throw new Error(g.f('Unexpected `undefined` in query'));
break;
case 'ignore':
default:
this.remove();
}
}
if (!Array.isArray(x) && (typeof x === 'object' && x !== null &&
x.constructor !== Object)) {
// This object is not a plain object
this.update(x, true); // Stop navigating into this object
return x;
}
return x;
});
}
var url = require('url');
var qs = require('qs');
/**
* Parse a URL into a settings object
* @param {String} urlStr The URL for connector settings
* @returns {Object} The settings object
*/
function parseSettings(urlStr) {
if (!urlStr) {
return {};
}
var uri = url.parse(urlStr, false);
var settings = {};
settings.connector = uri.protocol && uri.protocol.split(':')[0]; // Remove the trailing :
settings.host = settings.hostname = uri.hostname;
settings.port = uri.port && Number(uri.port); // port is a string
settings.user = settings.username = uri.auth && uri.auth.split(':')[0]; // <username>:<password>
settings.password = uri.auth && uri.auth.split(':')[1];
settings.database = uri.pathname && uri.pathname.split('/')[1]; // remove the leading /
settings.url = urlStr;
if (uri.query) {
var params = qs.parse(uri.query);
for (var p in params) {
settings[p] = params[p];
}
}
return settings;
}
/**
* Merge model settings
*
* Folked from https://github.com/nrf110/deepmerge/blob/master/index.js
*
* The original function tries to merge array items if they are objects
*
* @param {Object} target The target settings object
* @param {Object} src The source settings object
* @returns {Object} The merged settings object
*/
function mergeSettings(target, src) {
var array = Array.isArray(src);
var dst = array && [] || {};
Iif (array) {
target = target || [];
// Add items from target into dst
dst = dst.concat(target);
// Add non-existent items from source into dst
src.forEach(function(e) {
if (dst.indexOf(e) === -1) {
dst.push(e);
}
});
} else {
if (target != null && typeof target === 'object') {
// Add properties from target to dst
Object.keys(target).forEach(function(key) {
dst[key] = target[key];
});
}
Eif (src != null && typeof src === 'object') {
// Source is an object
Object.keys(src).forEach(function(key) {
var srcValue = src[key];
if (srcValue == null || typeof srcValue !== 'object') {
// The source item value is null, undefined or not an object
dst[key] = srcValue;
} else {
// The source item value is an object
Eif (target == null || typeof target !== 'object' ||
target[key] == null) {
// If target is not an object or target item value
dst[key] = srcValue;
} else {
dst[key] = mergeSettings(target[key], src[key]);
}
}
});
}
}
return dst;
}
/**
* Define an non-enumerable __cachedRelations property
* @param {Object} obj The obj to receive the __cachedRelations
*/
function defineCachedRelations(obj) {
if (!obj.__cachedRelations) {
Object.defineProperty(obj, '__cachedRelations', {
writable: true,
enumerable: false,
configurable: true,
value: {},
});
}
}
/**
* Check if the argument is plain object
* @param {*) obj The obj value
* @returns {boolean}
*/
function isPlainObject(obj) {
return (typeof obj === 'object') && (obj !== null) &&
(obj.constructor === Object);
}
function sortObjectsByIds(idName, ids, objects, strict) {
ids = ids.map(function(id) {
return (typeof id === 'object') ? String(id) : id;
});
var indexOf = function(x) {
var isObj = (typeof x[idName] === 'object'); // ObjectID
var id = isObj ? String(x[idName]) : x[idName];
return ids.indexOf(id);
};
var heading = [];
var tailing = [];
objects.forEach(function(x) {
if (typeof x === 'object') {
var idx = indexOf(x);
if (strict && idx === -1) return;
idx === -1 ? tailing.push(x) : heading.push(x);
}
});
heading.sort(function(x, y) {
var a = indexOf(x);
var b = indexOf(y);
if (a === -1 || b === -1) return 1; // last
if (a === b) return 0;
if (a > b) return 1;
if (a < b) return -1;
});
return heading.concat(tailing);
};
function createPromiseCallback() {
var cb;
var promise = new Promise(function(resolve, reject) {
cb = function(err, data) {
if (err) return reject(err);
return resolve(data);
};
});
cb.promise = promise;
return cb;
}
/**
* Dedupe an array
* @param {Array} an array
* @returns {Array} an array with unique items
*/
function uniq(a) {
var uniqArray = [];
if (!a) {
return uniqArray;
}
assert(Array.isArray(a), 'array argument is required');
for (var i = 0, n = a.length; i < n; i++) {
if (a.indexOf(a[i]) === i) {
uniqArray.push(a[i]);
}
}
return uniqArray;
}
/**
* Converts a string, regex literal, or a RegExp object to a RegExp object.
* @param {String|Object} The string, regex literal, or RegExp object to convert
* @returns {Object} A RegExp object
*/
function toRegExp(regex) {
var isString = typeof regex === 'string';
var isRegExp = regex instanceof RegExp;
if (!(isString || isRegExp))
return new Error(g.f('Invalid argument, must be a string, {{regex}} literal, or ' +
'{{RegExp}} object'));
if (isRegExp)
return regex;
if (!hasRegExpFlags(regex))
return new RegExp(regex);
// only accept i, g, or m as valid regex flags
var flags = regex.split('/').pop().split('');
var validFlags = ['i', 'g', 'm'];
var invalidFlags = [];
flags.forEach(function(flag) {
if (validFlags.indexOf(flag) === -1)
invalidFlags.push(flag);
});
var hasInvalidFlags = invalidFlags.length > 0;
if (hasInvalidFlags)
return new Error(g.f('Invalid {{regex}} flags: %s', invalidFlags));
// strip regex delimiter forward slashes
var expression = regex.substr(1, regex.lastIndexOf('/') - 1);
return new RegExp(expression, flags.join(''));
}
function hasRegExpFlags(regex) {
return regex instanceof RegExp ?
regex.toString().split('/').pop() :
!!regex.match(/.*\/.+$/);
}
// Compare two id values to decide if updateAttributes is trying to change
// the id value for a given instance
function idEquals(id1, id2) {
if (id1 === id2) {
return true;
}
// Allows number/string conversions
if ((typeof id1 === 'number' && typeof id2 === 'string') ||
(typeof id1 === 'string' && typeof id2 === 'number')) {
return id1 == id2;
}
// For complex id types such as MongoDB ObjectID
id1 = JSON.stringify(id1);
id2 = JSON.stringify(id2);
if (id1 === id2) {
return true;
}
return false;
}
// Defaults to native Array.prototype.indexOf when no idEqual is present
// Otherwise, returns the lowest index for which isEqual(arr[]index, target) is true
function findIndexOf(arr, target, isEqual) {
if (!isEqual) {
return arr.indexOf(target);
}
for (var i = 0; i < arr.length; i++) {
if (isEqual(arr[i], target)) { return i; }
};
return -1;
}
/**
* Returns an object that queries targetIds.
* @param {Array} The array of targetData
* @param {String} The Id property name of target model
* @returns {Object} The object that queries targetIds
*/
function collectTargetIds(targetData, idPropertyName) {
var targetIds = [];
for (var i = 0; i < targetData.length; i++) {
var targetId = targetData[i][idPropertyName];
targetIds.push(targetId);
};
var IdQuery = {
inq: uniq(targetIds),
};
return IdQuery;
}
/**
* Find the idKey of a Model.
* @param {ModelConstructor} m - Model Constructor
* @returns {String}
*/
function idName(m) {
return m.definition.idName() || 'id';
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 10 12 12 12 1 1 1 1 1 1 1 1 1 12 5 12 12 12 12 12 1 12 12 12 12 12 12 12 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('strong-globalize')();
var util = require('util');
var extend = util._extend;
/*!
* Module exports
*/
exports.ValidationError = ValidationError;
exports.Validatable = Validatable;
/**
* This class provides methods that add validation cababilities to models.
* Each of the validations runs when the `obj.isValid()` method is called.
*
* All of the methods have an options object parameter that has a
* `message` property. When there is only a single error message, this property is just a string;
* for example: `Post.validatesPresenceOf('title', { message: 'can not be blank' });`
*
* In more complicated cases it can be a set of messages, for each possible error condition; for example:
* `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
* @class Validatable
*/
function Validatable() {
}
/**
* Validate presence of one or more specified properties.
* Requires a model to include a property to be considered valid; fails when validated field is blank.
*
* For example, validate presence of title
* ```
* Post.validatesPresenceOf('title');
* ```
* Validate that model has first, last, and age properties:
* ```
* User.validatesPresenceOf('first', 'last', 'age');
* ```
* Example with custom message
* ```
* Post.validatesPresenceOf('title', {message: 'Cannot be blank'});
* ```
*
* @param {String} propertyName One or more property names.
* @options {Object} errMsg Optional custom error message. Default is "can't be blank"
* @property {String} message Error message to use instead of default.
*/
Validatable.validatesPresenceOf = getConfigurator('presence');
/**
* Validate absence of one or more specified properties.
* A model should not include a property to be considered valid; fails when validated field not blank.
*
* For example, validate absence of reserved
* ```
* Post.validatesAbsenceOf('reserved', { unless: 'special' });
* ```
* @param {String} propertyName One or more property names.
* @options {Object} errMsg Optional custom error message. Default is "can't be set"
* @property {String} message Error message to use instead of default.
*/
Validatable.validatesAbsenceOf = getConfigurator('absence');
/**
* Validate length. Require a property length to be within a specified range.
* Three kinds of validations: min, max, is.
*
* Default error messages:
*
* - min: too short
* - max: too long
* - is: length is wrong
*
* Example: length validations
* ```
* User.validatesLengthOf('password', {min: 7});
* User.validatesLengthOf('email', {max: 100});
* User.validatesLengthOf('state', {is: 2});
* User.validatesLengthOf('nick', {min: 3, max: 15});
* ```
* Example: length validations with custom error messages
* ```
* User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}});
* User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}});
* ```
* @param {String} propertyName Property name to validate.
* @options {Object} Options See below.
* @property {Number} is Value that property must equal to validate.
* @property {Number} min Value that property must be less than to be valid.
* @property {Number} max Value that property must be less than to be valid.
* @property {Object} message Optional Object with string properties for custom error message for each validation: is, min, or max
*/
Validatable.validatesLengthOf = getConfigurator('length');
/**
* Validate numericality. Requires a value for property to be either an integer or number.
*
* Example
* ```
* User.validatesNumericalityOf('age', { message: { number: '...' }});
* User.validatesNumericalityOf('age', {int: true, message: { int: '...' }});
* ```
*
* @param {String} propertyName Property name to validate.
* @options {Object} Options See below.
* @property {Boolean} int If true, then property must be an integer to be valid.
* @property {Object} message Optional object with string properties for 'int' for integer validation. Default error messages:
*
* - number: is not a number
* - int: is not an integer
*/
Validatable.validatesNumericalityOf = getConfigurator('numericality');
/**
* Validate inclusion in set. Require a value for property to be in the specified array.
*
* Example:
* ```
* User.validatesInclusionOf('gender', {in: ['male', 'female']});
* User.validatesInclusionOf('role', {
* in: ['admin', 'moderator', 'user'], message: 'is not allowed'
* });
* ```
*
* @param {String} propertyName Property name to validate.
* @options {Object} Options See below
* @property {Array} inArray Property must match one of the values in the array to be valid.
* @property {String} message Optional error message if property is not valid.
* Default error message: "is not included in the list".
* @property {Boolean} allowNull Whether null values are allowed.
*/
Validatable.validatesInclusionOf = getConfigurator('inclusion');
/**
* Validate exclusion. Require a property value not be in the specified array.
*
* Example: `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});`
*
* @param {String} propertyName Property name to validate.
* @options {Object} Options
* @property {Array} in Property must not match any of the values in the array to be valid.
* @property {String} message Optional error message if property is not valid. Default error message: "is reserved".
* @property {Boolean} allowNull Whether null values are allowed.
*/
Validatable.validatesExclusionOf = getConfigurator('exclusion');
/**
* Validate format. Require a model to include a property that matches the given format.
*
* Require a model to include a property that matches the given format. Example:
* `User.validatesFormatOf('name', {with: /\w+/});`
*
* @param {String} propertyName Property name to validate.
* @options {Object} Options
* @property {RegExp} with Regular expression to validate format.
* @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
* @property {Boolean} allowNull Whether null values are allowed.
*/
Validatable.validatesFormatOf = getConfigurator('format');
/**
* Validate using custom validation function.
*
* Example:
*
* User.validate('name', customValidator, {message: 'Bad name'});
* function customValidator(err) {
* if (this.name === 'bad') err();
* });
* var user = new User({name: 'Peter'});
* user.isValid(); // true
* user.name = 'bad';
* user.isValid(); // false
*
* @param {String} propertyName Property name to validate.
* @param {Function} validatorFn Custom validation function.
* @options {Object} Options See below.
* @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
* @property {Boolean} allowNull Whether null values are allowed.
*/
Validatable.validate = getConfigurator('custom');
/**
* Validate using custom asynchronous validation function.
*
*
* Example:
*```js
* User.validateAsync('name', customValidator, {message: 'Bad name'});
* function customValidator(err, done) {
* process.nextTick(function () {
* if (this.name === 'bad') err();
* done();
* });
* });
* var user = new User({name: 'Peter'});
* user.isValid(); // false (because async validation setup)
* user.isValid(function (isValid) {
* isValid; // true
* })
* user.name = 'bad';
* user.isValid(); // false
* user.isValid(function (isValid) {
* isValid; // false
* })
*```
* @param {String} propertyName Property name to validate.
* @param {Function} validatorFn Custom validation function.
* @options {Object} Options See below
* @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
* @property {Boolean} allowNull Whether null values are allowed.
*/
Validatable.validateAsync = getConfigurator('custom', {async: true});
/**
* Validate uniqueness. Ensure the value for property is unique in the collection of models.
* Not available for all connectors. Currently supported with these connectors:
* - In Memory
* - Oracle
* - MongoDB
*
* ```
* // The login must be unique across all User instances.
* User.validatesUniquenessOf('login');
*
* // Assuming SiteUser.belongsTo(Site)
* // The login must be unique within each Site.
* SiteUser.validateUniquenessOf('login', { scopedTo: ['siteId'] });
* ```
* @param {String} propertyName Property name to validate.
* @options {Object} Options See below.
* @property {RegExp} with Regular expression to validate format.
* @property {Array.<String>} scopedTo List of properties defining the scope.
* @property {String} message Optional error message if property is not valid. Default error message: "is not unique".
* @property {Boolean} allowNull Whether null values are allowed.
*/
Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
// implementation of validators
/*!
* Presence validator
*/
function validatePresence(attr, conf, err, options) {
if (blank(this[attr])) {
err();
}
}
/*!
* Absence validator
*/
function validateAbsence(attr, conf, err, options) {
if (!blank(this[attr])) {
err();
}
}
/*!
* Length validator
*/
function validateLength(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
var len = this[attr].length;
if (conf.min && len < conf.min) {
err('min');
}
if (conf.max && len > conf.max) {
err('max');
}
if (conf.is && len !== conf.is) {
err('is');
}
}
/*!
* Numericality validator
*/
function validateNumericality(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] !== 'number' || isNaN(this[attr])) {
return err('number');
}
if (conf.int && this[attr] !== Math.round(this[attr])) {
return err('int');
}
}
/*!
* Inclusion validator
*/
function validateInclusion(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (!~conf.in.indexOf(this[attr])) {
err();
}
}
/*!
* Exclusion validator
*/
function validateExclusion(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (~conf.in.indexOf(this[attr])) {
err();
}
}
/*!
* Format validator
*/
function validateFormat(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
}
} else {
err();
}
}
/*!
* Custom validator
*/
function validateCustom(attr, conf, err, options, done) {
if (typeof options === 'function') {
done = options;
options = {};
}
conf.customValidator.call(this, err, done);
}
/*!
* Uniqueness validator
*/
function validateUniqueness(attr, conf, err, options, done) {
if (typeof options === 'function') {
done = options;
options = {};
}
if (blank(this[attr])) {
return process.nextTick(done);
}
var cond = {where: {}};
cond.where[attr] = this[attr];
if (conf && conf.scopedTo) {
conf.scopedTo.forEach(function(k) {
var val = this[k];
if (val !== undefined)
cond.where[k] = this[k];
}, this);
}
var idName = this.constructor.definition.idName();
var isNewRecord = this.isNewRecord();
this.constructor.find(cond, options, function(error, found) {
if (error) {
err(error);
} else if (found.length > 1) {
err();
} else if (found.length === 1 && idName === attr && isNewRecord) {
err();
} else if (found.length === 1 && (
!this.id || !found[0].id || found[0].id.toString() != this.id.toString()
)) {
err();
}
done();
}.bind(this));
}
var validators = {
presence: validatePresence,
absence: validateAbsence,
length: validateLength,
numericality: validateNumericality,
inclusion: validateInclusion,
exclusion: validateExclusion,
format: validateFormat,
custom: validateCustom,
uniqueness: validateUniqueness,
};
function getConfigurator(name, opts) {
return function() {
var args = Array.prototype.slice.call(arguments);
args[1] = args[1] || {};
configure(this, name, args, opts);
};
}
/**
* This method performs validation and triggers validation hooks.
* Before validation the `obj.errors` collection is cleaned.
* Each validation can add errors to `obj.errors` collection.
* If collection is not blank, validation failed.
*
* NOTE: This method can be called as synchronous only when no asynchronous validation is
* configured. It's strongly recommended to run all validations as asyncronous.
*
* Example: ExpressJS controller: render user if valid, show flash otherwise
* ```
* user.isValid(function (valid) {
* if (valid) res.render({user: user});
* else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users');
* });
* ```
* Another example:
* ```
* user.isValid(function (valid) {
* if (!valid) {
* console.log(user.errors);
* // => hash of errors
* // => {
* // => username: [errmessage, errmessage, ...],
* // => email: ...
* // => }
* }
* });
* ```
* @param {Function} callback called with (valid)
* @returns {Boolean} True if no asynchronous validation is configured and all properties pass validation.
*/
Validatable.prototype.isValid = function(callback, data, options) {
options = options || {};
var valid = true, inst = this, wait = 0, async = false;
var validations = this.constructor.validations;
var reportDiscardedProperties = this.__strict &&
this.__unknownProperties && this.__unknownProperties.length;
// exit with success when no errors
if (typeof validations !== 'object' && !reportDiscardedProperties) {
cleanErrors(this);
if (callback) {
this.trigger('validate', function(validationsDone) {
validationsDone.call(inst, function() {
callback(valid);
});
}, data, callback);
}
return valid;
}
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: new Errors,
});
this.trigger('validate', function(validationsDone) {
var inst = this,
asyncFail = false;
var attrs = Object.keys(validations || {});
attrs.forEach(function(attr) {
var attrValidations = validations[attr] || [];
attrValidations.forEach(function(v) {
if (v.options && v.options.async) {
async = true;
wait += 1;
process.nextTick(function() {
validationFailed(inst, attr, v, options, done);
});
} else {
if (validationFailed(inst, attr, v)) {
valid = false;
}
}
});
});
if (reportDiscardedProperties) {
for (var ix in inst.__unknownProperties) {
var key = inst.__unknownProperties[ix];
var code = 'unknown-property';
var msg = defaultMessages[code];
inst.errors.add(key, msg, code);
valid = false;
}
}
if (!async) {
validationsDone.call(inst, function() {
if (valid) cleanErrors(inst);
if (callback) {
callback(valid);
}
});
}
function done(fail) {
asyncFail = asyncFail || fail;
if (--wait === 0) {
validationsDone.call(inst, function() {
if (valid && !asyncFail) cleanErrors(inst);
if (callback) {
callback(valid && !asyncFail);
}
});
}
}
}, data, callback);
if (async) {
// in case of async validation we should return undefined here,
// because not all validations are finished yet
return;
} else {
return valid;
}
};
function cleanErrors(inst) {
Object.defineProperty(inst, 'errors', {
enumerable: false,
configurable: true,
value: false,
});
}
function validationFailed(inst, attr, conf, options, cb) {
var opts = conf.options || {};
if (typeof options === 'function') {
cb = options;
options = {};
}
if (typeof attr !== 'string') return false;
// here we should check skip validation conditions (if, unless)
// that can be specified in conf
if (skipValidation(inst, conf, 'if') ||
skipValidation(inst, conf, 'unless')) {
if (cb) cb(false);
return false;
}
var fail = false;
var validator = validators[conf.validation];
var validatorArguments = [];
validatorArguments.push(attr);
validatorArguments.push(conf);
validatorArguments.push(function onerror(kind) {
var message, code = conf.code || conf.validation;
if (conf.message) {
message = conf.message;
}
if (!message && defaultMessages[conf.validation]) {
message = defaultMessages[conf.validation];
}
if (!message) {
message = 'is invalid';
}
if (kind) {
code += '.' + kind;
if (message[kind]) {
// get deeper
message = message[kind];
} else if (defaultMessages.common[kind]) {
message = defaultMessages.common[kind];
} else {
message = 'is invalid';
}
}
if (kind !== false) inst.errors.add(attr, message, code);
fail = true;
});
validatorArguments.push(options);
if (cb) {
validatorArguments.push(function() {
cb(fail);
});
}
validator.apply(inst, validatorArguments);
return fail;
}
function skipValidation(inst, conf, kind) {
var doValidate = true;
if (typeof conf[kind] === 'function') {
doValidate = conf[kind].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else if (typeof conf[kind] === 'string') {
if (typeof inst[conf[kind]] === 'function') {
doValidate = inst[conf[kind]].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else if (inst.__data.hasOwnProperty(conf[kind])) {
doValidate = inst[conf[kind]];
if (kind === 'unless') doValidate = !doValidate;
} else {
doValidate = kind === 'if';
}
}
return !doValidate;
}
var defaultMessages = {
presence: 'can\'t be blank',
absence: 'can\'t be set',
'unknown-property': 'is not defined in the model',
length: {
min: 'too short',
max: 'too long',
is: 'length is wrong',
},
common: {
blank: 'is blank',
'null': 'is null',
},
numericality: {
'int': 'is not an integer',
'number': 'is not a number',
},
inclusion: 'is not included in the list',
exclusion: 'is reserved',
uniqueness: 'is not unique',
};
/**
* Checks if attribute is undefined or null. Calls err function with 'blank' or 'null'.
* See defaultMessages. You can affect this behaviour with conf.allowBlank and conf.allowNull.
* @param {String} attr Property name of attribute
* @param {Object} conf conf object for validator
* @param {Function} err
* @return {Boolean} returns true if attribute is null or blank
*/
function nullCheck(attr, conf, err) {
// First determine if attribute is defined
if (typeof this[attr] === 'undefined') {
if (!conf.allowBlank) {
err('blank');
}
return true;
} else {
// Now check if attribute is null
if (this[attr] === null) {
if (!conf.allowNull) {
err('null');
}
return true;
}
}
return false;
}
/*!
* Return true when v is undefined, blank array, null or empty string
* otherwise returns false
*
* @param {Mix} v
* Returns true if `v` is blank.
*/
function blank(v) {
if (typeof v === 'undefined') return true;
if (v instanceof Array && v.length === 0) return true;
if (v === null) return true;
if (typeof v === 'number' && isNaN(v)) return true;
if (typeof v == 'string' && v === '') return true;
return false;
}
function configure(cls, validation, args, opts) {
if (!cls.validations) {
Object.defineProperty(cls, 'validations', {
writable: true,
configurable: true,
enumerable: false,
value: {},
});
}
args = [].slice.call(args);
var conf;
Eif (typeof args[args.length - 1] === 'object') {
conf = args.pop();
} else {
conf = {};
}
if (validation === 'custom' && typeof args[args.length - 1] === 'function') {
conf.customValidator = args.pop();
}
conf.validation = validation;
args.forEach(function(attr) {
Eif (typeof attr === 'string') {
var validation = extend({}, conf);
validation.options = opts || {};
cls.validations[attr] = cls.validations[attr] || [];
cls.validations[attr].push(validation);
}
});
}
function Errors() {
Object.defineProperty(this, 'codes', {
enumerable: false,
configurable: true,
value: {},
});
}
Errors.prototype.add = function(field, message, code) {
code = code || 'invalid';
if (!this[field]) {
this[field] = [];
this.codes[field] = [];
}
this[field].push(message);
this.codes[field].push(code);
};
function ErrorCodes(messages) {
var c = this;
Object.keys(messages).forEach(function(field) {
c[field] = messages[field].codes;
});
}
/**
* ValidationError is raised when the application attempts to save an invalid model instance.
* Example:
* ```
* {
* "name": "ValidationError",
* "status": 422,
* "message": "The Model instance is not valid. \
* See `details` property of the error object for more info.",
* "statusCode": 422,
* "details": {
* "context": "user",
* "codes": {
* "password": [
* "presence"
* ],
* "email": [
* "uniqueness"
* ]
* },
* "messages": {
* "password": [
* "can't be blank"
* ],
* "email": [
* "Email already exists"
* ]
* }
* },
* }
* ```
* You might run into situations where you need to raise a validation error yourself, for example in a "before" hook or a
* custom model method.
* ```
* MyModel.prototype.preflight = function(changes, callback) {
* // Update properties, do not save to db
* for (var key in changes) {
* model[key] = changes[key];
* }
*
* if (model.isValid()) {
* return callback(null, { success: true });
* }
*
* // This line shows how to create a ValidationError
* var err = new MyModel.ValidationError(model);
* callback(err);
* }
* ```
*/
function ValidationError(obj) {
if (!(this instanceof ValidationError)) return new ValidationError(obj);
this.name = 'ValidationError';
var context = obj && obj.constructor && obj.constructor.modelName;
this.message = g.f(
'The %s instance is not valid. Details: %s.',
context ? '`' + context + '`' : 'model',
formatErrors(obj.errors, obj.toJSON()) || '(unknown)'
);
this.statusCode = 422;
this.details = {
context: context,
codes: obj.errors && obj.errors.codes,
messages: obj.errors,
};
if (Error.captureStackTrace) {
// V8 (Chrome, Opera, Node)
Error.captureStackTrace(this, this.constructor);
} else if (errorHasStackProperty) {
// Firefox
this.stack = (new Error).stack;
}
// Safari and PhantomJS initializes `error.stack` on throw
// Internet Explorer does not support `error.stack`
}
util.inherits(ValidationError, Error);
var errorHasStackProperty = !!(new Error).stack;
ValidationError.maxPropertyStringLength = 32;
function formatErrors(errors, propertyValues) {
var DELIM = '; ';
errors = errors || {};
return Object.getOwnPropertyNames(errors)
.filter(function(propertyName) {
return Array.isArray(errors[propertyName]);
})
.map(function(propertyName) {
var messages = errors[propertyName];
var propertyValue = propertyValues[propertyName];
return messages.map(function(msg) {
return formatPropertyError(propertyName, propertyValue, msg);
}).join(DELIM);
})
.join(DELIM);
}
function formatPropertyError(propertyName, propertyValue, errorMessage) {
var formattedValue;
var valueType = typeof propertyValue;
if (valueType === 'string') {
formattedValue = JSON.stringify(truncatePropertyString(propertyValue));
} else if (propertyValue instanceof Date) {
formattedValue = propertyValue.toISOString();
} else if (valueType === 'object') {
// objects and arrays
formattedValue = util.inspect(propertyValue, {
showHidden: false,
color: false,
// show top-level object properties only
depth: Array.isArray(propertyValue) ? 1 : 0,
});
formattedValue = truncatePropertyString(formattedValue);
} else {
formattedValue = truncatePropertyString('' + propertyValue);
}
return '`' + propertyName + '` ' + errorMessage +
' (value: ' + formattedValue + ')';
}
function truncatePropertyString(value) {
var len = ValidationError.maxPropertyStringLength;
if (value.length <= len) return value;
// preserve few last characters like `}` or `]`, but no more than 3
// this way the last `} ]` in the array of objects is included in the message
var tail;
var m = value.match(/([ \t})\]]+)$/);
if (m) {
tail = m[1].slice(-3);
len -= tail.length;
} else {
tail = value.slice(-3);
len -= 3;
}
return value.slice(0, len - 4) + '...' + tail;
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| memory.js | 10.16% | (57 / 561) | 0% | (0 / 360) | 0% | (0 / 94) | 10.44% | (57 / 546) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/* global window:false */
var g = require('strong-globalize')();
var util = require('util');
var Connector = require('loopback-connector').Connector;
var geo = require('../geo');
var utils = require('../utils');
var fs = require('fs');
var async = require('async');
var debug = require('debug')('loopback:connector:memory');
/**
* Initialize the Memory connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
dataSource.connector = new Memory(null, dataSource.settings);
// Use dataSource.connect to avoid duplicate file reads from cache
dataSource.connect(callback);
};
exports.Memory = Memory;
exports.applyFilter = applyFilter;
function Memory(m, settings) {
if (m instanceof Memory) {
this.isTransaction = true;
this.cache = m.cache;
this.ids = m.ids;
this.constructor.super_.call(this, 'memory', settings);
this._models = m._models;
} else {
this.isTransaction = false;
this.cache = {};
this.ids = {};
this.constructor.super_.call(this, 'memory', settings);
}
}
util.inherits(Memory, Connector);
Memory.prototype.getDefaultIdType = function() {
return Number;
};
Memory.prototype.getTypes = function() {
return ['db', 'nosql', 'memory'];
};
Memory.prototype.connect = function(callback) {
if (this.isTransaction) {
this.onTransactionExec = callback;
} else {
this.loadFromFile(callback);
}
};
function serialize(obj) {
if (obj === null || obj === undefined) {
return obj;
}
return JSON.stringify(obj);
}
function deserialize(dbObj) {
if (dbObj === null || dbObj === undefined) {
return dbObj;
}
if (typeof dbObj === 'string') {
return JSON.parse(dbObj);
} else {
return dbObj;
}
}
Memory.prototype.getCollection = function(model) {
var modelClass = this._models[model];
if (modelClass && modelClass.settings.memory) {
model = modelClass.settings.memory.collection || model;
}
return model;
};
Memory.prototype.initCollection = function(model) {
this.collection(model, {});
this.collectionSeq(model, 1);
};
Memory.prototype.collection = function(model, val) {
model = this.getCollection(model);
if (arguments.length > 1) this.cache[model] = val;
return this.cache[model];
};
Memory.prototype.collectionSeq = function(model, val) {
model = this.getCollection(model);
if (arguments.length > 1) this.ids[model] = val;
return this.ids[model];
};
/**
* Create a queue to serialize file read/write operations
* @returns {*} The file operation queue
*/
Memory.prototype.setupFileQueue = function() {
var self = this;
if (!this.fileQueue) {
// Create a queue for writes
this.fileQueue = async.queue(function(task, done) {
var callback = task.callback || function() {};
var file = self.settings.file;
if (task.operation === 'write') {
// Flush out the models/ids
var data = JSON.stringify({
ids: self.ids,
models: self.cache,
}, null, ' ');
debug('Writing cache to %s: %s', file, data);
fs.writeFile(file, data, function(err) {
debug('Cache has been written to %s', file);
done(err);
callback(err, task.data);
});
} else if (task.operation === 'read') {
debug('Reading cache from %s: %s', file, data);
fs.readFile(file, {
encoding: 'utf8',
flag: 'r',
}, function(err, data) {
if (err && err.code !== 'ENOENT') {
done(err);
callback(err);
} else {
debug('Cache has been read from %s: %s', file, data);
self.parseAndLoad(data, function(err) {
done(err);
callback(err);
});
}
});
} else {
var err = new Error('Unknown type of task');
done(err);
callback(err);
}
}, 1);
}
return this.fileQueue;
};
Memory.prototype.parseAndLoad = function(data, callback) {
if (data) {
try {
data = JSON.parse(data.toString());
} catch (e) {
return callback && callback(e);
}
this.ids = data.ids || {};
this.cache = data.models || {};
} else {
if (!this.cache) {
this.ids = {};
this.cache = {};
}
}
callback && callback();
};
Memory.prototype.loadFromFile = function(callback) {
var hasLocalStorage = typeof window !== 'undefined' && window.localStorage;
var localStorage = hasLocalStorage && this.settings.localStorage;
if (this.settings.file) {
debug('Queueing read %s', this.settings.file);
this.setupFileQueue().push({
operation: 'read',
callback: callback,
});
} else if (localStorage) {
var data = window.localStorage.getItem(localStorage);
data = data || '{}';
this.parseAndLoad(data, callback);
} else {
process.nextTick(callback);
}
};
/*!
* Flush the cache into the json file if necessary
* @param {Function} callback
*/
Memory.prototype.saveToFile = function(result, callback) {
var file = this.settings.file;
var hasLocalStorage = typeof window !== 'undefined' && window.localStorage;
var localStorage = hasLocalStorage && this.settings.localStorage;
if (file) {
debug('Queueing write %s', this.settings.file);
// Enqueue the write
this.setupFileQueue().push({
operation: 'write',
data: result,
callback: callback,
});
} else if (localStorage) {
// Flush out the models/ids
var data = JSON.stringify({
ids: this.ids,
models: this.cache,
}, null, ' ');
window.localStorage.setItem(localStorage, data);
process.nextTick(function() {
callback && callback(null, result);
});
} else {
process.nextTick(function() {
callback && callback(null, result);
});
}
};
Memory.prototype.define = function defineModel(definition) {
this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments));
var m = definition.model.modelName;
if (!this.collection(m)) this.initCollection(m);
};
Memory.prototype._createSync = function(model, data, fn) {
// FIXME: [rfeng] We need to generate unique ids based on the id type
// FIXME: [rfeng] We don't support composite ids yet
var currentId = this.collectionSeq(model);
if (currentId === undefined) { // First time
currentId = this.collectionSeq(model, 1);
}
var id = this.getIdValue(model, data) || currentId;
if (id > currentId) {
// If the id is passed in and the value is greater than the current id
currentId = id;
}
this.collectionSeq(model, Number(currentId) + 1);
var props = this._models[model].properties;
var idName = this.idName(model);
id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
this.setIdValue(model, data, id);
if (!this.collection(model)) {
this.collection(model, {});
}
if (this.collection(model)[id]) {
var error = new Error(g.f('Duplicate entry for %s.%s', model, idName));
error.statusCode = error.status = 409;
return fn(error);
}
this.collection(model)[id] = serialize(data);
fn(null, id);
};
Memory.prototype.create = function create(model, data, options, callback) {
var self = this;
this._createSync(model, data, function(err, id) {
if (err) {
return process.nextTick(function() {
callback(err);
});
};
self.saveToFile(id, callback);
});
};
Memory.prototype.updateOrCreate = function(model, data, options, callback) {
var self = this;
this.exists(model, self.getIdValue(model, data), options, function(err, exists) {
if (exists) {
self.save(model, data, options, function(err, data) {
callback(err, data, {isNewInstance: false});
});
} else {
self.create(model, data, options, function(err, id) {
self.setIdValue(model, data, id);
callback(err, data, {isNewInstance: true});
});
}
});
};
Memory.prototype.patchOrCreateWithWhere =
Memory.prototype.upsertWithWhere = function(model, where, data, options, callback) {
var self = this;
var primaryKey = this.idName(model);
var filter = {where: where};
var nodes = self._findAllSkippingIncludes(model, filter);
if (nodes.length === 0) {
return self._createSync(model, data, function(err, id) {
if (err) return process.nextTick(function() { callback(err); });
self.saveToFile(id, function(err, id) {
self.setIdValue(model, data, id);
callback(err, self.fromDb(model, data), {isNewInstance: true});
});
});
}
if (nodes.length === 1) {
var primaryKeyValue = nodes[0][primaryKey];
self.updateAttributes(model, primaryKeyValue, data, options, function(err, data) {
callback(err, data, {isNewInstance: false});
});
} else {
process.nextTick(function() {
var error = new Error('There are multiple instances found.' +
'Upsert Operation will not be performed!');
error.statusCode = 400;
callback(error);
});
}
};
Memory.prototype.findOrCreate = function(model, filter, data, callback) {
var self = this;
var nodes = self._findAllSkippingIncludes(model, filter);
var found = nodes[0];
if (!found) {
// Calling _createSync to update the collection in a sync way and to guarantee to create it in the same turn of even loop
return self._createSync(model, data, function(err, id) {
if (err) return callback(err);
self.saveToFile(id, function(err, id) {
self.setIdValue(model, data, id);
callback(err, data, true);
});
});
}
if (!filter || !filter.include) {
return process.nextTick(function() {
callback(null, found, false);
});
}
self._models[model].model.include(nodes[0], filter.include, {}, function(err, nodes) {
process.nextTick(function() {
if (err) return callback(err);
callback(null, nodes[0], false);
});
});
};
Memory.prototype.save = function save(model, data, options, callback) {
var self = this;
var id = this.getIdValue(model, data);
var cachedModels = this.collection(model);
var modelData = cachedModels && this.collection(model)[id];
modelData = modelData && deserialize(modelData);
if (modelData) {
data = merge(modelData, data);
}
this.collection(model)[id] = serialize(data);
this.saveToFile(data, function(err) {
callback(err, self.fromDb(model, data), {isNewInstance: !modelData});
});
};
Memory.prototype.exists = function exists(model, id, options, callback) {
process.nextTick(function() {
callback(null, this.collection(model) && this.collection(model).hasOwnProperty(id));
}.bind(this));
};
Memory.prototype.find = function find(model, id, options, callback) {
process.nextTick(function() {
callback(null, id in this.collection(model) && this.fromDb(model, this.collection(model)[id]));
}.bind(this));
};
Memory.prototype.destroy = function destroy(model, id, options, callback) {
var exists = this.collection(model)[id];
delete this.collection(model)[id];
this.saveToFile({count: exists ? 1 : 0}, callback);
};
Memory.prototype.fromDb = function(model, data) {
if (!data) return null;
data = deserialize(data);
var props = this._models[model].properties;
for (var key in data) {
var val = data[key];
if (val === undefined || val === null) {
continue;
}
if (props[key]) {
switch (props[key].type.name) {
case 'Date':
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
break;
case 'Boolean':
val = Boolean(val);
break;
case 'Number':
val = Number(val);
break;
}
}
data[key] = val;
}
return data;
};
function getValue(obj, path) {
if (obj == null) {
return undefined;
}
var keys = path.split('.');
var val = obj;
for (var i = 0, n = keys.length; i < n; i++) {
val = val[keys[i]];
if (val == null) {
return val;
}
}
return val;
}
Memory.prototype._findAllSkippingIncludes = function(model, filter) {
var nodes = Object.keys(this.collection(model)).map(function(key) {
return this.fromDb(model, this.collection(model)[key]);
}.bind(this));
if (filter) {
if (!filter.order) {
var idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
}
// do we need some sorting?
if (filter.order) {
var orders = filter.order;
if (typeof filter.order === 'string') {
orders = [filter.order];
}
orders.forEach(function(key, i) {
var reverse = 1;
var m = key.match(/\s+(A|DE)SC$/i);
if (m) {
key = key.replace(/\s+(A|DE)SC/i, '');
if (m[1].toLowerCase() === 'de') reverse = -1;
}
orders[i] = {'key': key, 'reverse': reverse};
});
nodes = nodes.sort(sorting.bind(orders));
}
var nearFilter = geo.nearFilter(filter.where);
// geo sorting
if (nearFilter) {
nodes = geo.filter(nodes, nearFilter);
}
// do we need some filtration?
if (filter.where && nodes)
nodes = nodes.filter(applyFilter(filter));
// field selection
if (filter.fields) {
nodes = nodes.map(utils.selectFields(filter.fields));
}
// limit/skip
var skip = filter.skip || filter.offset || 0;
var limit = filter.limit || nodes.length;
nodes = nodes.slice(skip, skip + limit);
}
return nodes;
function sorting(a, b) {
var undefinedA, undefinedB;
for (var i = 0, l = this.length; i < l; i++) {
var aVal = getValue(a, this[i].key);
var bVal = getValue(b, this[i].key);
undefinedB = bVal === undefined && aVal !== undefined;
undefinedA = aVal === undefined && bVal !== undefined;
if (undefinedB || aVal > bVal) {
return 1 * this[i].reverse;
} else if (undefinedA || aVal < bVal) {
return -1 * this[i].reverse;
}
}
return 0;
}
};
Memory.prototype.all = function all(model, filter, options, callback) {
var self = this;
var nodes = self._findAllSkippingIncludes(model, filter);
process.nextTick(function() {
if (filter && filter.include) {
self._models[model].model.include(nodes, filter.include, options, callback);
} else {
callback(null, nodes);
}
});
};
function applyFilter(filter) {
var where = filter.where;
if (typeof where === 'function') {
return where;
}
var keys = Object.keys(where);
return function(obj) {
return keys.every(function(key) {
if (key === 'and' || key === 'or') {
if (Array.isArray(where[key])) {
if (key === 'and') {
return where[key].every(function(cond) {
return applyFilter({where: cond})(obj);
});
}
if (key === 'or') {
return where[key].some(function(cond) {
return applyFilter({where: cond})(obj);
});
}
}
}
var value = getValue(obj, key);
// Support referencesMany and other embedded relations
// Also support array types. Mongo, possibly PostgreSQL
if (Array.isArray(value)) {
var matcher = where[key];
// The following condition is for the case where we are querying with
// a neq filter, and when the value is an empty array ([]).
if (matcher.neq !== undefined && value.length <= 0) {
return true;
}
return value.some(function(v, i) {
var filter = {where: {}};
filter.where[i] = matcher;
return applyFilter(filter)(value);
});
}
if (test(where[key], value)) {
return true;
}
// If we have a composed key a.b and b would resolve to a property of an object inside an array
// then, we attempt to emulate mongo db matching. Helps for embedded relations
var dotIndex = key.indexOf('.');
var subValue = obj[key.substring(0, dotIndex)];
if (dotIndex !== -1) {
var subFilter = {where: {}};
var subKey = key.substring(dotIndex + 1);
subFilter.where[subKey] = where[key];
if (Array.isArray(subValue)) {
return subValue.some(applyFilter(subFilter));
} else if (typeof subValue === 'object' && subValue !== null) {
return applyFilter(subFilter)(subValue);
}
}
return false;
});
};
function toRegExp(pattern) {
if (pattern instanceof RegExp) {
return pattern;
}
var regex = '';
// Escaping user input to be treated as a literal string within a regular expression
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Writing_a_Regular_Expression_Pattern
pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
for (var i = 0, n = pattern.length; i < n; i++) {
var char = pattern.charAt(i);
if (char === '\\') {
i++; // Skip to next char
if (i < n) {
regex += pattern.charAt(i);
}
continue;
} else if (char === '%') {
regex += '.*';
} else if (char === '_') {
regex += '.';
} else if (char === '.') {
regex += '\\.';
} else if (char === '*') {
regex += '\\*';
} else {
regex += char;
}
}
return regex;
}
function test(example, value) {
if (typeof value === 'string' && (example instanceof RegExp)) {
return value.match(example);
}
if (example === undefined) {
return undefined;
}
if (typeof example === 'object' && example !== null) {
if (example.regexp) {
return value ? value.match(example.regexp) : false;
}
// ignore geo near filter
if (example.near) {
return true;
}
var i;
if (example.inq) {
// if (!value) return false;
for (i = 0; i < example.inq.length; i++) {
if (example.inq[i] == value) {
return true;
}
}
return false;
}
if (example.nin) {
for (i = 0; i < example.nin.length; i++) {
if (example.nin[i] == value) {
return false;
}
}
return true;
}
if ('neq' in example) {
return compare(example.neq, value) !== 0;
}
if ('between' in example) {
return (testInEquality({gte: example.between[0]}, value) &&
testInEquality({lte: example.between[1]}, value));
}
if (example.like || example.nlike || example.ilike || example.nilike) {
var like = example.like || example.nlike || example.ilike || example.nilike;
if (typeof like === 'string') {
like = toRegExp(like);
}
if (example.like) {
return !!new RegExp(like).test(value);
}
if (example.nlike) {
return !new RegExp(like).test(value);
}
if (example.ilike) {
return !!new RegExp(like, 'i').test(value);
}
if (example.nilike) {
return !new RegExp(like, 'i').test(value);
}
}
if (testInEquality(example, value)) {
return true;
}
}
// not strict equality
return (example !== null ? example.toString() : example) ==
(value != null ? value.toString() : value);
}
/**
* Compare two values
* @param {*} val1 The 1st value
* @param {*} val2 The 2nd value
* @returns {number} 0: =, positive: >, negative <
* @private
*/
function compare(val1, val2) {
if (val1 == null || val2 == null) {
// Either val1 or val2 is null or undefined
return val1 == val2 ? 0 : NaN;
}
if (typeof val1 === 'number') {
return val1 - val2;
}
if (typeof val1 === 'string') {
return (val1 > val2) ? 1 : ((val1 < val2) ? -1 : (val1 == val2) ? 0 : NaN);
}
if (typeof val1 === 'boolean') {
return val1 - val2;
}
if (val1 instanceof Date) {
var result = val1 - val2;
return result;
}
// Return NaN if we don't know how to compare
return (val1 == val2) ? 0 : NaN;
}
function testInEquality(example, val) {
if ('gt' in example) {
return compare(val, example.gt) > 0;
}
if ('gte' in example) {
return compare(val, example.gte) >= 0;
}
if ('lt' in example) {
return compare(val, example.lt) < 0;
}
if ('lte' in example) {
return compare(val, example.lte) <= 0;
}
return false;
}
}
Memory.prototype.destroyAll = function destroyAll(model, where, options, callback) {
var cache = this.collection(model);
var filter = null;
var count = 0;
if (where) {
filter = applyFilter({where: where});
Object.keys(cache).forEach(function(id) {
if (!filter || filter(this.fromDb(model, cache[id]))) {
count++;
delete cache[id];
}
}.bind(this));
} else {
count = Object.keys(cache).length;
this.collection(model, {});
}
this.saveToFile({count: count}, callback);
};
Memory.prototype.count = function count(model, where, options, callback) {
var cache = this.collection(model);
var data = Object.keys(cache);
if (where) {
var filter = {where: where};
data = data.map(function(id) {
return this.fromDb(model, cache[id]);
}.bind(this));
data = data.filter(applyFilter(filter));
}
process.nextTick(function() {
callback(null, data.length);
});
};
Memory.prototype.update =
Memory.prototype.updateAll = function updateAll(model, where, data, options, cb) {
var self = this;
var cache = this.collection(model);
var filter = null;
where = where || {};
filter = applyFilter({where: where});
var ids = Object.keys(cache);
var count = 0;
async.each(ids, function(id, done) {
var inst = self.fromDb(model, cache[id]);
if (!filter || filter(inst)) {
count++;
// The id value from the cache is string
// Get the real id from the inst
id = self.getIdValue(model, inst);
self.updateAttributes(model, id, data, options, done);
} else {
process.nextTick(done);
}
}, function(err) {
if (err) return cb(err);
self.saveToFile({count: count}, cb);
});
};
Memory.prototype.updateAttributes = function updateAttributes(model, id, data, options, cb) {
if (!id) {
var err = new Error(g.f('You must provide an {{id}} when updating attributes!'));
if (cb) {
return cb(err);
} else {
throw err;
}
}
// Do not modify the data object passed in arguments
data = Object.create(data);
this.setIdValue(model, data, id);
var cachedModels = this.collection(model);
var modelData = cachedModels && this.collection(model)[id];
if (modelData) {
this.save(model, data, options, cb);
} else {
cb(new Error(g.f('Could not update attributes. {{Object}} with {{id}} %s does not exist!', id)));
}
};
Memory.prototype.replaceById = function(model, id, data, options, cb) {
var self = this;
if (!id) {
var err = new Error(g.f('You must provide an {{id}} when replacing!'));
return process.nextTick(function() { cb(err); });
}
// Do not modify the data object passed in arguments
data = Object.create(data);
this.setIdValue(model, data, id);
var cachedModels = this.collection(model);
var modelData = cachedModels && this.collection(model)[id];
if (!modelData) {
var msg = 'Could not replace. Object with id ' + id + ' does not exist!';
var error = new Error(msg);
error.statusCode = error.status = 404;
return process.nextTick(function() { cb(error); });
}
var newModelData = {};
for (var key in data) {
var val = data[key];
if (typeof val === 'function') {
continue; // Skip methods
}
newModelData[key] = val;
}
this.collection(model)[id] = serialize(newModelData);
this.saveToFile(newModelData, function(err) {
cb(err, self.fromDb(model, newModelData));
});
};
Memory.prototype.replaceOrCreate = function(model, data, options, callback) {
var self = this;
var idName = self.idNames(model)[0];
var idValue = self.getIdValue(model, data);
var filter = {where: {}};
filter.where[idName] = idValue;
var nodes = self._findAllSkippingIncludes(model, filter);
var found = nodes[0];
if (!found) {
// Calling _createSync to update the collection in a sync way and
// to guarantee to create it in the same turn of even loop
return self._createSync(model, data, function(err, id) {
if (err) return process.nextTick(function() { callback(err); });
self.saveToFile(id, function(err, id) {
self.setIdValue(model, data, id);
callback(err, self.fromDb(model, data), {isNewInstance: true});
});
});
}
var id = self.getIdValue(model, data);
self.collection(model)[id] = serialize(data);
self.saveToFile(data, function(err) {
callback(err, self.fromDb(model, data), {isNewInstance: false});
});
};
Memory.prototype.transaction = function() {
return new Memory(this);
};
Memory.prototype.exec = function(callback) {
this.onTransactionExec();
setTimeout(callback, 50);
};
Memory.prototype.buildNearFilter = function(filter) {
// noop
};
Memory.prototype.automigrate = function(models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(self._models);
if (models.length === 0) {
return process.nextTick(cb);
}
var invalidModels = models.filter(function(m) {
return !(m in self._models);
});
if (invalidModels.length) {
return process.nextTick(function() {
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
}
models.forEach(function(m) {
self.initCollection(m);
});
if (cb) process.nextTick(cb);
};
function merge(base, update) {
if (!base) {
return update;
}
// We cannot use Object.keys(update) if the update is an instance of the model
// class as the properties are defined at the ModelClass.prototype level
for (var key in update) {
var val = update[key];
if (typeof val === 'function') {
continue; // Skip methods
}
base[key] = val;
}
return base;
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| delete-all.js | 20.51% | (8 / 39) | 0% | (0 / 18) | 0% | (0 / 5) | 22.22% | (8 / 36) | |
| delete.js | 19.05% | (4 / 21) | 0% | (0 / 12) | 0% | (0 / 2) | 19.05% | (4 / 21) | |
| expire.js | 21.43% | (3 / 14) | 0% | (0 / 12) | 0% | (0 / 1) | 21.43% | (3 / 14) | |
| get.js | 23.08% | (3 / 13) | 0% | (0 / 10) | 0% | (0 / 2) | 23.08% | (3 / 13) | |
| index.js | 91.67% | (11 / 12) | 100% | (0 / 0) | 0% | (0 / 2) | 91.67% | (11 / 12) | |
| iterate-keys.js | 25% | (3 / 12) | 0% | (0 / 6) | 0% | (0 / 2) | 25% | (3 / 12) | |
| keys.js | 15.38% | (4 / 26) | 0% | (0 / 18) | 0% | (0 / 2) | 16.67% | (4 / 24) | |
| set.js | 16.67% | (3 / 18) | 0% | (0 / 18) | 0% | (0 / 1) | 16.67% | (3 / 18) | |
| ttl.js | 23.08% | (3 / 13) | 0% | (0 / 10) | 0% | (0 / 1) | 23.08% | (3 / 13) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | 1 1 1 1 1 1 1 1 | 'use strict';
var assert = require('assert');
var async = require('async');
var debug = require('debug')('loopback:kvao:delete-all');
var utils = require('../utils');
/**
* Delete all keys (and values) associated to the current model.
*
* @options {Object} options Unused ATM, placeholder for future options.
* @callback {Function} callback
* @param {Error} err Error object.
* @promise
*
* @header KVAO.prototype.deleteAll([options, ]cb)
*/
module.exports = function deleteAll(options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
assert(typeof options === 'object', 'options must be an object');
callback = callback || utils.createPromiseCallback();
var connector = this.getConnector();
if (typeof connector.deleteAll === 'function') {
connector.deleteAll(this.modelName, options, callback);
} else if (typeof connector.delete === 'function') {
debug('Falling back to unoptimized key-value pair deletion');
iterateAndDelete(connector, this.modelName, options, callback);
} else {
var errMsg = 'Connector does not support key-value pair deletion';
debug(errMsg);
process.nextTick(function() {
var err = new Error(errMsg);
err.statusCode = 501;
callback(err);
});
}
return callback.promise;
};
function iterateAndDelete(connector, modelName, options, callback) {
var iter = connector.iterateKeys(modelName, {});
var keys = [];
iter.next(onNextKey);
function onNextKey(err, key) {
if (err) return callback(err);
if (key === undefined) return callback();
connector.delete(modelName, key, options, onDeleted);
}
function onDeleted(err) {
if (err) return callback(err);
iter.next(onNextKey);
}
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 1 1 1 1 | 'use strict';
var assert = require('assert');
var debug = require('debug')('loopback:kvao:delete');
var utils = require('../utils');
/**
* Delete the key-value pair associated to the given key.
*
* @param {String} key Key to use when searching the database.
* @options {Object} options
* @callback {Function} callback
* @param {Error} err Error object.
* @param {*} result Value associated with the given key.
* @promise
*
* @header KVAO.prototype.delete(key[, options], cb)
*/
module.exports = function keyValueDelete(key, options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
assert(typeof key === 'string' && key, 'key must be a non-empty string');
callback = callback || utils.createPromiseCallback();
var connector = this.getConnector();
if (typeof connector.delete === 'function') {
connector.delete(this.modelName, key, options, callback);
} else {
var errMsg = 'Connector does not support key-value pair deletion';
debug(errMsg);
process.nextTick(function() {
var err = new Error(errMsg);
err.statusCode = 501;
callback(err);
});
}
return callback.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 1 1 1 | 'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Set the TTL (time to live) in ms (milliseconds) for a given key. TTL is the
* remaining time before a key-value pair is discarded from the database.
*
* @param {String} key Key to use when searching the database.
* @param {Number} ttl TTL in ms to set for the key.
* @options {Object} options
* @callback {Function} callback
* @param {Error} err Error object.
* @promise
*
* @header KVAO.expire(key, ttl, cb)
*/
module.exports = function keyValueExpire(key, ttl, options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
assert(typeof key === 'string' && key, 'key must be a non-empty string');
assert(typeof ttl === 'number' && ttl > 0, 'ttl must be a positive integer');
assert(typeof options === 'object', 'options must be an object');
callback = callback || utils.createPromiseCallback();
this.getConnector().expire(this.modelName, key, ttl, options, callback);
return callback.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 1 1 1 | 'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Return the value associated with a given key.
*
* @param {String} key Key to use when searching the database.
* @options {Object} options
* @callback {Function} callback
* @param {Error} err Error object.
* @param {*} result Value associated with the given key.
* @promise
*
* @header KVAO.get(key, cb)
*/
module.exports = function keyValueGet(key, options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
assert(typeof key === 'string' && key, 'key must be a non-empty string');
callback = callback || utils.createPromiseCallback();
this.getConnector().get(this.modelName, key, options, function(err, result) {
// TODO convert raw result to Model instance (?)
callback(err, result);
});
return callback.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 1 1 1 1 1 1 1 1 1 1 1 | 'use strict'; function KeyValueAccessObject() { }; module.exports = KeyValueAccessObject; KeyValueAccessObject.delete = require('./delete'); KeyValueAccessObject.deleteAll = require('./delete-all'); KeyValueAccessObject.get = require('./get'); KeyValueAccessObject.set = require('./set'); KeyValueAccessObject.expire = require('./expire'); KeyValueAccessObject.ttl = require('./ttl'); KeyValueAccessObject.iterateKeys = require('./iterate-keys'); KeyValueAccessObject.keys = require('./keys'); KeyValueAccessObject.getConnector = function() { return this.getDataSource().connector; }; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 1 1 1 | 'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Asynchronously iterate all keys in the database. Similar to `.keys()` but
* instead allows for iteration over large data sets without having to load
* everything into memory at once.
*
* @param {Object} filter An optional filter object with the following
* @param {String} filter.match Glob string to use to filter returned
* keys (i.e. `userid.*`). All connectors are required to support `*` and
* `?`. They may also support additional special characters that are
* specific to the backing database.
* @param {Object} options
* @returns {AsyncIterator} An Object implementing `next(cb) -> Promise`
* function that can be used to iterate all keys.
*
* @header KVAO.iterateKeys(filter)
*/
module.exports = function keyValueIterateKeys(filter, options) {
filter = filter || {};
options = options || {};
assert(typeof filter === 'object', 'filter must be an object');
assert(typeof options === 'object', 'options must be an object');
var iter = this.getConnector().iterateKeys(this.modelName, filter, options);
// promisify the returned iterator
return {
next: function(callback) {
callback = callback || utils.createPromiseCallback();
iter.next(callback);
return callback.promise;
},
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 1 1 1 1 | 'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Return all keys in the database.
*
* **WARNING**: This method is not suitable for large data sets as all
* key-values pairs are loaded into memory at once. For large data sets,
* use `iterateKeys()` instead.
*
* @param {Object} filter An optional filter object with the following
* @param {String} filter.match Glob string used to filter returned
* keys (i.e. `userid.*`). All connectors are required to support `*` and
* `?`, but may also support additional special characters specific to the
* database.
* @param {Object} options
* @callback {Function} callback
* @promise
*
*
* @header KVAO.keys(filter, callback)
*/
module.exports = function keyValueKeys(filter, options, callback) {
if (callback === undefined) {
if (typeof options === 'function') {
callback = options;
options = undefined;
} else if (options === undefined && typeof filter === 'function') {
callback = filter;
filter = undefined;
}
}
filter = filter || {};
options = options || {};
assert(typeof filter === 'object', 'filter must be an object');
assert(typeof options === 'object', 'options must be an object');
callback = callback || utils.createPromiseCallback();
var iter = this.iterateKeys(filter, options);
var keys = [];
iter.next(onNextKey);
function onNextKey(err, key) {
if (err) return callback(err);
if (key === undefined) return callback(null, keys);
keys.push(key);
iter.next(onNextKey);
}
return callback.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 1 1 1 | 'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Persist a value and associate it with the given key.
*
* @param {String} key Key to associate with the given value.
* @param {*} value Value to persist.
* @options {Number|Object} options Optional settings for the key-value
* pair. If a Number is provided, it is set as the TTL (time to live) in ms
* (milliseconds) for the key-value pair.
* @property {Number} ttl TTL for the key-value pair in ms.
* @callback {Function} callback
* @param {Error} err Error object.
* @promise
*
* @header KVAO.set(key, value, cb)
*/
module.exports = function keyValueSet(key, value, options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options === 'number') {
options = {ttl: options};
} else if (!options) {
options = {};
}
assert(typeof key === 'string' && key, 'key must be a non-empty string');
assert(value != null, 'value must be defined and not null');
assert(typeof options === 'object', 'options must be an object');
if (options && 'ttl' in options) {
assert(typeof options.ttl === 'number' && options.ttl > 0,
'options.ttl must be a positive number');
}
callback = callback || utils.createPromiseCallback();
// TODO convert possible model instance in "value" to raw data via toObect()
this.getConnector().set(this.modelName, key, value, options, callback);
return callback.promise;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 1 1 1 | 'use strict';
var assert = require('assert');
var utils = require('../utils');
/**
* Return the TTL (time to live) for a given key. TTL is the remaining time
* before a key-value pair is discarded from the database.
*
* @param {String} key Key to use when searching the database.
* @options {Object} options
* @callback {Function} callback
* @param {Error} error
* @param {Number} ttl Expiration time for the key-value pair. `undefined` if
* TTL was not initially set.
* @promise
*
* @header KVAO.ttl(key, cb)
*/
module.exports = function keyValueTtl(key, options, callback) {
if (callback == undefined && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
assert(typeof key === 'string' && key, 'key must be a non-empty string');
assert(typeof options === 'object', 'options must be an object');
callback = callback || utils.createPromiseCallback();
this.getConnector().ttl(this.modelName, key, options, callback);
return callback.promise;
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| async.js | 34.96% | (431 / 1233) | 10.49% | (64 / 610) | 7.75% | (21 / 271) | 36.1% | (430 / 1191) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 4702 4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 4980 4981 4982 4983 4984 4985 4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 5092 5093 5094 5095 5096 5097 5098 5099 5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 5133 5134 5135 5136 5137 5138 5139 5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200 5201 5202 5203 5204 5205 5206 5207 5208 5209 5210 5211 5212 5213 5214 5215 5216 5217 5218 5219 5220 5221 5222 5223 5224 5225 5226 5227 5228 5229 5230 5231 5232 5233 5234 5235 5236 5237 5238 5239 5240 5241 5242 5243 5244 5245 5246 5247 5248 5249 5250 5251 5252 5253 5254 5255 5256 5257 5258 5259 5260 5261 5262 5263 5264 5265 5266 5267 5268 5269 5270 5271 5272 5273 5274 5275 5276 5277 5278 5279 5280 5281 5282 5283 5284 5285 5286 5287 5288 5289 5290 5291 | 1 1 1 1 1 10 10 1 1 10 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 13 1 1 1 1 1 7 1 1 1 1 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 6 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.async = global.async || {})));
}(this, (function (exports) { 'use strict';
/**
* A faster alternative to `Function#apply`, this function invokes `func`
* with the `this` binding of `thisArg` and the arguments of `args`.
*
* @private
* @param {Function} func The function to invoke.
* @param {*} thisArg The `this` binding of `func`.
* @param {Array} args The arguments to invoke `func` with.
* @returns {*} Returns the result of `func`.
*/
function apply(func, thisArg, args) {
switch (args.length) {
case 0: return func.call(thisArg);
case 1: return func.call(thisArg, args[0]);
case 2: return func.call(thisArg, args[0], args[1]);
case 3: return func.call(thisArg, args[0], args[1], args[2]);
}
return func.apply(thisArg, args);
}
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max;
/**
* A specialized version of `baseRest` which transforms the rest array.
*
* @private
* @param {Function} func The function to apply a rest parameter to.
* @param {number} [start=func.length-1] The start position of the rest parameter.
* @param {Function} transform The rest array transform.
* @returns {Function} Returns the new function.
*/
function overRest$1(func, start, transform) {
start = nativeMax(start === undefined ? (func.length - 1) : start, 0);
return function() {
var args = arguments,
index = -1,
length = nativeMax(args.length - start, 0),
array = Array(length);
while (++index < length) {
array[index] = args[start + index];
}
index = -1;
var otherArgs = Array(start + 1);
while (++index < start) {
otherArgs[index] = args[index];
}
otherArgs[start] = transform(array);
return apply(func, this, otherArgs);
};
}
/**
* This method returns the first argument it receives.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Util
* @param {*} value Any value.
* @returns {*} Returns `value`.
* @example
*
* var object = { 'a': 1 };
*
* console.log(_.identity(object) === object);
* // => true
*/
function identity(value) {
return value;
}
// Lodash rest function without function.toString()
// remappings
function rest(func, start) {
return overRest$1(func, start, identity);
}
var initialParams = function (fn) {
return rest(function (args /*..., callback*/) {
var callback = args.pop();
fn.call(this, args, callback);
});
};
function applyEach$1(eachfn) {
return rest(function (fns, args) {
var go = initialParams(function (args, callback) {
var that = this;
return eachfn(fns, function (fn, cb) {
fn.apply(that, args.concat(cb));
}, callback);
});
if (args.length) {
return go.apply(this, args);
} else {
return go;
}
});
}
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
/** Built-in value references. */
var Symbol$1 = root.Symbol;
/** Used for built-in method references. */
var objectProto = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString = objectProto.toString;
/** Built-in value references. */
var symToStringTag$1 = Symbol$1 ? Symbol$1.toStringTag : undefined;
/**
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the raw `toStringTag`.
*/
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag$1),
tag = value[symToStringTag$1];
try {
value[symToStringTag$1] = undefined;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag$1] = tag;
} else {
delete value[symToStringTag$1];
}
}
return result;
}
/** Used for built-in method references. */
var objectProto$1 = Object.prototype;
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString$1 = objectProto$1.toString;
/**
* Converts `value` to a string using `Object.prototype.toString`.
*
* @private
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
*/
function objectToString(value) {
return nativeObjectToString$1.call(value);
}
/** `Object#toString` result references. */
var nullTag = '[object Null]';
var undefinedTag = '[object Undefined]';
/** Built-in value references. */
var symToStringTag = Symbol$1 ? Symbol$1.toStringTag : undefined;
/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value) {
Iif (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
/** `Object#toString` result references. */
var asyncTag = '[object AsyncFunction]';
var funcTag = '[object Function]';
var genTag = '[object GeneratorFunction]';
var proxyTag = '[object Proxy]';
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
if (!isObject(value)) {
return false;
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER = 9007199254740991;
/**
* Checks if `value` is a valid array-like length.
*
* **Note:** This method is loosely based on
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
* @example
*
* _.isLength(3);
* // => true
*
* _.isLength(Number.MIN_VALUE);
* // => false
*
* _.isLength(Infinity);
* // => false
*
* _.isLength('3');
* // => false
*/
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
/**
* Checks if `value` is array-like. A value is considered array-like if it's
* not a function and has a `value.length` that's an integer greater than or
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is array-like, else `false`.
* @example
*
* _.isArrayLike([1, 2, 3]);
* // => true
*
* _.isArrayLike(document.body.children);
* // => true
*
* _.isArrayLike('abc');
* // => true
*
* _.isArrayLike(_.noop);
* // => false
*/
function isArrayLike(value) {
return value != null && isLength(value.length) && !isFunction(value);
}
// A temporary value used to identify if the loop should be broken.
// See #1064, #1293
var breakLoop = {};
/**
* This method returns `undefined`.
*
* @static
* @memberOf _
* @since 2.3.0
* @category Util
* @example
*
* _.times(2, _.noop);
* // => [undefined, undefined]
*/
function noop() {
// No operation performed.
}
function once(fn) {
return function () {
if (fn === null) return;
var callFn = fn;
fn = null;
callFn.apply(this, arguments);
};
}
var iteratorSymbol = typeof Symbol === 'function' && Symbol.iterator;
var getIterator = function (coll) {
return iteratorSymbol && coll[iteratorSymbol] && coll[iteratorSymbol]();
};
/**
* The base implementation of `_.times` without support for iteratee shorthands
* or max array length checks.
*
* @private
* @param {number} n The number of times to invoke `iteratee`.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the array of results.
*/
function baseTimes(n, iteratee) {
var index = -1,
result = Array(n);
while (++index < n) {
result[index] = iteratee(index);
}
return result;
}
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value) {
return value != null && typeof value == 'object';
}
/** `Object#toString` result references. */
var argsTag = '[object Arguments]';
/**
* The base implementation of `_.isArguments`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
*/
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
/** Used for built-in method references. */
var objectProto$3 = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty$2 = objectProto$3.hasOwnProperty;
/** Built-in value references. */
var propertyIsEnumerable = objectProto$3.propertyIsEnumerable;
/**
* Checks if `value` is likely an `arguments` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
* else `false`.
* @example
*
* _.isArguments(function() { return arguments; }());
* // => true
*
* _.isArguments([1, 2, 3]);
* // => false
*/
var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
return isObjectLike(value) && hasOwnProperty$2.call(value, 'callee') &&
!propertyIsEnumerable.call(value, 'callee');
};
/**
* Checks if `value` is classified as an `Array` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array, else `false`.
* @example
*
* _.isArray([1, 2, 3]);
* // => true
*
* _.isArray(document.body.children);
* // => false
*
* _.isArray('abc');
* // => false
*
* _.isArray(_.noop);
* // => false
*/
var isArray = Array.isArray;
/**
* This method returns `false`.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {boolean} Returns `false`.
* @example
*
* _.times(2, _.stubFalse);
* // => [false, false]
*/
function stubFalse() {
return false;
}
/** Detect free variable `exports`. */
var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
/** Detect free variable `module`. */
var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;
/** Built-in value references. */
var Buffer = moduleExports ? root.Buffer : undefined;
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined;
/**
* Checks if `value` is a buffer.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
* @example
*
* _.isBuffer(new Buffer(2));
* // => true
*
* _.isBuffer(new Uint8Array(2));
* // => false
*/
var isBuffer = nativeIsBuffer || stubFalse;
/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER$1 = 9007199254740991;
/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;
/**
* Checks if `value` is a valid array-like index.
*
* @private
* @param {*} value The value to check.
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
length = length == null ? MAX_SAFE_INTEGER$1 : length;
return !!length &&
(typeof value == 'number' || reIsUint.test(value)) &&
(value > -1 && value % 1 == 0 && value < length);
}
/** `Object#toString` result references. */
var argsTag$1 = '[object Arguments]';
var arrayTag = '[object Array]';
var boolTag = '[object Boolean]';
var dateTag = '[object Date]';
var errorTag = '[object Error]';
var funcTag$1 = '[object Function]';
var mapTag = '[object Map]';
var numberTag = '[object Number]';
var objectTag = '[object Object]';
var regexpTag = '[object RegExp]';
var setTag = '[object Set]';
var stringTag = '[object String]';
var weakMapTag = '[object WeakMap]';
var arrayBufferTag = '[object ArrayBuffer]';
var dataViewTag = '[object DataView]';
var float32Tag = '[object Float32Array]';
var float64Tag = '[object Float64Array]';
var int8Tag = '[object Int8Array]';
var int16Tag = '[object Int16Array]';
var int32Tag = '[object Int32Array]';
var uint8Tag = '[object Uint8Array]';
var uint8ClampedTag = '[object Uint8ClampedArray]';
var uint16Tag = '[object Uint16Array]';
var uint32Tag = '[object Uint32Array]';
/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
typedArrayTags[uint32Tag] = true;
typedArrayTags[argsTag$1] = typedArrayTags[arrayTag] =
typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
typedArrayTags[errorTag] = typedArrayTags[funcTag$1] =
typedArrayTags[mapTag] = typedArrayTags[numberTag] =
typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
typedArrayTags[setTag] = typedArrayTags[stringTag] =
typedArrayTags[weakMapTag] = false;
/**
* The base implementation of `_.isTypedArray` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
*/
function baseIsTypedArray(value) {
return isObjectLike(value) &&
isLength(value.length) && !!typedArrayTags[baseGetTag(value)];
}
/**
* The base implementation of `_.unary` without support for storing metadata.
*
* @private
* @param {Function} func The function to cap arguments for.
* @returns {Function} Returns the new capped function.
*/
function baseUnary(func) {
return function(value) {
return func(value);
};
}
/** Detect free variable `exports`. */
var freeExports$1 = typeof exports == 'object' && exports && !exports.nodeType && exports;
/** Detect free variable `module`. */
var freeModule$1 = freeExports$1 && typeof module == 'object' && module && !module.nodeType && module;
/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports$1 = freeModule$1 && freeModule$1.exports === freeExports$1;
/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports$1 && freeGlobal.process;
/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
try {
return freeProcess && freeProcess.binding && freeProcess.binding('util');
} catch (e) {}
}());
/* Node.js helper references. */
var nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
/**
* Checks if `value` is classified as a typed array.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
* @example
*
* _.isTypedArray(new Uint8Array);
* // => true
*
* _.isTypedArray([]);
* // => false
*/
var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
/** Used for built-in method references. */
var objectProto$2 = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty$1 = objectProto$2.hasOwnProperty;
/**
* Creates an array of the enumerable property names of the array-like `value`.
*
* @private
* @param {*} value The value to query.
* @param {boolean} inherited Specify returning inherited property names.
* @returns {Array} Returns the array of property names.
*/
function arrayLikeKeys(value, inherited) {
var isArr = isArray(value),
isArg = !isArr && isArguments(value),
isBuff = !isArr && !isArg && isBuffer(value),
isType = !isArr && !isArg && !isBuff && isTypedArray(value),
skipIndexes = isArr || isArg || isBuff || isType,
result = skipIndexes ? baseTimes(value.length, String) : [],
length = result.length;
for (var key in value) {
if ((inherited || hasOwnProperty$1.call(value, key)) &&
!(skipIndexes && (
// Safari 9 has enumerable `arguments.length` in strict mode.
key == 'length' ||
// Node.js 0.10 has enumerable non-index properties on buffers.
(isBuff && (key == 'offset' || key == 'parent')) ||
// PhantomJS 2 has enumerable non-index properties on typed arrays.
(isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
// Skip index properties.
isIndex(key, length)
))) {
result.push(key);
}
}
return result;
}
/** Used for built-in method references. */
var objectProto$5 = Object.prototype;
/**
* Checks if `value` is likely a prototype object.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
*/
function isPrototype(value) {
var Ctor = value && value.constructor,
proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto$5;
return value === proto;
}
/**
* Creates a unary function that invokes `func` with its argument transformed.
*
* @private
* @param {Function} func The function to wrap.
* @param {Function} transform The argument transform.
* @returns {Function} Returns the new function.
*/
function overArg(func, transform) {
return function(arg) {
return func(transform(arg));
};
}
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeKeys = overArg(Object.keys, Object);
/** Used for built-in method references. */
var objectProto$4 = Object.prototype;
/** Used to check objects for own properties. */
var hasOwnProperty$3 = objectProto$4.hasOwnProperty;
/**
* The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeys(object) {
if (!isPrototype(object)) {
return nativeKeys(object);
}
var result = [];
for (var key in Object(object)) {
if (hasOwnProperty$3.call(object, key) && key != 'constructor') {
result.push(key);
}
}
return result;
}
/**
* Creates an array of the own enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects. See the
* [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* for more details.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keys(new Foo);
* // => ['a', 'b'] (iteration order is not guaranteed)
*
* _.keys('hi');
* // => ['0', '1']
*/
function keys(object) {
return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}
function createArrayIterator(coll) {
var i = -1;
var len = coll.length;
return function next() {
return ++i < len ? { value: coll[i], key: i } : null;
};
}
function createES2015Iterator(iterator) {
var i = -1;
return function next() {
var item = iterator.next();
if (item.done) return null;
i++;
return { value: item.value, key: i };
};
}
function createObjectIterator(obj) {
var okeys = keys(obj);
var i = -1;
var len = okeys.length;
return function next() {
var key = okeys[++i];
return i < len ? { value: obj[key], key: key } : null;
};
}
function iterator(coll) {
if (isArrayLike(coll)) {
return createArrayIterator(coll);
}
var iterator = getIterator(coll);
return iterator ? createES2015Iterator(iterator) : createObjectIterator(coll);
}
function onlyOnce(fn) {
return function () {
if (fn === null) throw new Error("Callback was already called.");
var callFn = fn;
fn = null;
callFn.apply(this, arguments);
};
}
function _eachOfLimit(limit) {
return function (obj, iteratee, callback) {
callback = once(callback || noop);
if (limit <= 0 || !obj) {
return callback(null);
}
var nextElem = iterator(obj);
var done = false;
var running = 0;
function iterateeCallback(err, value) {
running -= 1;
if (err) {
done = true;
callback(err);
} else if (value === breakLoop || done && running <= 0) {
done = true;
return callback(null);
} else {
replenish();
}
}
function replenish() {
while (running < limit && !done) {
var elem = nextElem();
if (elem === null) {
done = true;
if (running <= 0) {
callback(null);
}
return;
}
running += 1;
iteratee(elem.value, elem.key, onlyOnce(iterateeCallback));
}
}
replenish();
};
}
/**
* The same as [`eachOf`]{@link module:Collections.eachOf} but runs a maximum of `limit` async operations at a
* time.
*
* @name eachOfLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.eachOf]{@link module:Collections.eachOf}
* @alias forEachOfLimit
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A function to apply to each
* item in `coll`. The `key` is the item's key, or index in the case of an
* array. The iteratee is passed a `callback(err)` which must be called once it
* has completed. If no error has occurred, the callback should be run without
* arguments or with an explicit `null` argument. Invoked with
* (item, key, callback).
* @param {Function} [callback] - A callback which is called when all
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
*/
function eachOfLimit(coll, limit, iteratee, callback) {
_eachOfLimit(limit)(coll, iteratee, callback);
}
function doLimit(fn, limit) {
return function (iterable, iteratee, callback) {
return fn(iterable, limit, iteratee, callback);
};
}
// eachOf implementation optimized for array-likes
function eachOfArrayLike(coll, iteratee, callback) {
callback = once(callback || noop);
var index = 0,
completed = 0,
length = coll.length;
if (length === 0) {
callback(null);
}
function iteratorCallback(err, value) {
if (err) {
callback(err);
} else if (++completed === length || value === breakLoop) {
callback(null);
}
}
for (; index < length; index++) {
iteratee(coll[index], index, onlyOnce(iteratorCallback));
}
}
// a generic version of eachOf which can handle array, object, and iterator cases.
var eachOfGeneric = doLimit(eachOfLimit, Infinity);
/**
* Like [`each`]{@link module:Collections.each}, except that it passes the key (or index) as the second argument
* to the iteratee.
*
* @name eachOf
* @static
* @memberOf module:Collections
* @method
* @alias forEachOf
* @category Collection
* @see [async.each]{@link module:Collections.each}
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each
* item in `coll`. The `key` is the item's key, or index in the case of an
* array. The iteratee is passed a `callback(err)` which must be called once it
* has completed. If no error has occurred, the callback should be run without
* arguments or with an explicit `null` argument. Invoked with
* (item, key, callback).
* @param {Function} [callback] - A callback which is called when all
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
* @example
*
* var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
* var configs = {};
*
* async.forEachOf(obj, function (value, key, callback) {
* fs.readFile(__dirname + value, "utf8", function (err, data) {
* if (err) return callback(err);
* try {
* configs[key] = JSON.parse(data);
* } catch (e) {
* return callback(e);
* }
* callback();
* });
* }, function (err) {
* if (err) console.error(err.message);
* // configs is now a map of JSON data
* doSomethingWith(configs);
* });
*/
var eachOf = function (coll, iteratee, callback) {
var eachOfImplementation = isArrayLike(coll) ? eachOfArrayLike : eachOfGeneric;
eachOfImplementation(coll, iteratee, callback);
};
function doParallel(fn) {
return function (obj, iteratee, callback) {
return fn(eachOf, obj, iteratee, callback);
};
}
function _asyncMap(eachfn, arr, iteratee, callback) {
callback = callback || noop;
arr = arr || [];
var results = [];
var counter = 0;
eachfn(arr, function (value, _, callback) {
var index = counter++;
iteratee(value, function (err, v) {
results[index] = v;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
/**
* Produces a new collection of values by mapping each value in `coll` through
* the `iteratee` function. The `iteratee` is called with an item from `coll`
* and a callback for when it has finished processing. Each of these callback
* takes 2 arguments: an `error`, and the transformed item from `coll`. If
* `iteratee` passes an error to its callback, the main `callback` (for the
* `map` function) is immediately called with the error.
*
* Note, that since this function applies the `iteratee` to each item in
* parallel, there is no guarantee that the `iteratee` functions will complete
* in order. However, the results array will be in the same order as the
* original `coll`.
*
* If `map` is passed an Object, the results will be an Array. The results
* will roughly be in the order of the original Objects' keys (but this can
* vary across JavaScript engines)
*
* @name map
* @static
* @memberOf module:Collections
* @method
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item in `coll`.
* The iteratee is passed a `callback(err, transformed)` which must be called
* once it has completed with an error (which can be `null`) and a
* transformed item. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. Results is an Array of the
* transformed items from the `coll`. Invoked with (err, results).
* @example
*
* async.map(['file1','file2','file3'], fs.stat, function(err, results) {
* // results is now an array of stats for each file
* });
*/
var map = doParallel(_asyncMap);
/**
* Applies the provided arguments to each function in the array, calling
* `callback` after all functions have completed. If you only provide the first
* argument, `fns`, then it will return a function which lets you pass in the
* arguments as if it were a single function call. If more arguments are
* provided, `callback` is required while `args` is still optional.
*
* @name applyEach
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Array|Iterable|Object} fns - A collection of asynchronous functions
* to all call with the same arguments
* @param {...*} [args] - any number of separate arguments to pass to the
* function.
* @param {Function} [callback] - the final argument should be the callback,
* called when all functions have completed processing.
* @returns {Function} - If only the first argument, `fns`, is provided, it will
* return a function which lets you pass in the arguments as if it were a single
* function call. The signature is `(..args, callback)`. If invoked with any
* arguments, `callback` is required.
* @example
*
* async.applyEach([enableSearch, updateSchema], 'bucket', callback);
*
* // partial application example:
* async.each(
* buckets,
* async.applyEach([enableSearch, updateSchema]),
* callback
* );
*/
var applyEach = applyEach$1(map);
function doParallelLimit(fn) {
return function (obj, limit, iteratee, callback) {
return fn(_eachOfLimit(limit), obj, iteratee, callback);
};
}
/**
* The same as [`map`]{@link module:Collections.map} but runs a maximum of `limit` async operations at a time.
*
* @name mapLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.map]{@link module:Collections.map}
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A function to apply to each item in `coll`.
* The iteratee is passed a `callback(err, transformed)` which must be called
* once it has completed with an error (which can be `null`) and a transformed
* item. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. Results is an array of the
* transformed items from the `coll`. Invoked with (err, results).
*/
var mapLimit = doParallelLimit(_asyncMap);
/**
* The same as [`map`]{@link module:Collections.map} but runs only a single async operation at a time.
*
* @name mapSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.map]{@link module:Collections.map}
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item in `coll`.
* The iteratee is passed a `callback(err, transformed)` which must be called
* once it has completed with an error (which can be `null`) and a
* transformed item. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. Results is an array of the
* transformed items from the `coll`. Invoked with (err, results).
*/
var mapSeries = doLimit(mapLimit, 1);
/**
* The same as [`applyEach`]{@link module:ControlFlow.applyEach} but runs only a single async operation at a time.
*
* @name applyEachSeries
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.applyEach]{@link module:ControlFlow.applyEach}
* @category Control Flow
* @param {Array|Iterable|Object} fns - A collection of asynchronous functions to all
* call with the same arguments
* @param {...*} [args] - any number of separate arguments to pass to the
* function.
* @param {Function} [callback] - the final argument should be the callback,
* called when all functions have completed processing.
* @returns {Function} - If only the first argument is provided, it will return
* a function which lets you pass in the arguments as if it were a single
* function call.
*/
var applyEachSeries = applyEach$1(mapSeries);
/**
* Creates a continuation function with some arguments already applied.
*
* Useful as a shorthand when combined with other control flow functions. Any
* arguments passed to the returned function are added to the arguments
* originally passed to apply.
*
* @name apply
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} function - The function you want to eventually apply all
* arguments to. Invokes with (arguments...).
* @param {...*} arguments... - Any number of arguments to automatically apply
* when the continuation is called.
* @example
*
* // using apply
* async.parallel([
* async.apply(fs.writeFile, 'testfile1', 'test1'),
* async.apply(fs.writeFile, 'testfile2', 'test2')
* ]);
*
*
* // the same process without using apply
* async.parallel([
* function(callback) {
* fs.writeFile('testfile1', 'test1', callback);
* },
* function(callback) {
* fs.writeFile('testfile2', 'test2', callback);
* }
* ]);
*
* // It's possible to pass any number of additional arguments when calling the
* // continuation:
*
* node> var fn = async.apply(sys.puts, 'one');
* node> fn('two', 'three');
* one
* two
* three
*/
var apply$2 = rest(function (fn, args) {
return rest(function (callArgs) {
return fn.apply(null, args.concat(callArgs));
});
});
/**
* Take a sync function and make it async, passing its return value to a
* callback. This is useful for plugging sync functions into a waterfall,
* series, or other async functions. Any arguments passed to the generated
* function will be passed to the wrapped function (except for the final
* callback argument). Errors thrown will be passed to the callback.
*
* If the function passed to `asyncify` returns a Promise, that promises's
* resolved/rejected state will be used to call the callback, rather than simply
* the synchronous return value.
*
* This also means you can asyncify ES2016 `async` functions.
*
* @name asyncify
* @static
* @memberOf module:Utils
* @method
* @alias wrapSync
* @category Util
* @param {Function} func - The synchronous function to convert to an
* asynchronous function.
* @returns {Function} An asynchronous wrapper of the `func`. To be invoked with
* (callback).
* @example
*
* // passing a regular synchronous function
* async.waterfall([
* async.apply(fs.readFile, filename, "utf8"),
* async.asyncify(JSON.parse),
* function (data, next) {
* // data is the result of parsing the text.
* // If there was a parsing error, it would have been caught.
* }
* ], callback);
*
* // passing a function returning a promise
* async.waterfall([
* async.apply(fs.readFile, filename, "utf8"),
* async.asyncify(function (contents) {
* return db.model.create(contents);
* }),
* function (model, next) {
* // `model` is the instantiated model object.
* // If there was an error, this function would be skipped.
* }
* ], callback);
*
* // es6 example
* var q = async.queue(async.asyncify(async function(file) {
* var intermediateStep = await processFile(file);
* return await somePromise(intermediateStep)
* }));
*
* q.push(files);
*/
function asyncify(func) {
return initialParams(function (args, callback) {
var result;
try {
result = func.apply(this, args);
} catch (e) {
return callback(e);
}
// if result is Promise object
if (isObject(result) && typeof result.then === 'function') {
result.then(function (value) {
callback(null, value);
}, function (err) {
callback(err.message ? err : new Error(err));
});
} else {
callback(null, result);
}
});
}
/**
* A specialized version of `_.forEach` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns `array`.
*/
function arrayEach(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break;
}
}
return array;
}
/**
* Creates a base function for methods like `_.forIn` and `_.forOwn`.
*
* @private
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Function} Returns the new base function.
*/
function createBaseFor(fromRight) {
return function(object, iteratee, keysFunc) {
var index = -1,
iterable = Object(object),
props = keysFunc(object),
length = props.length;
while (length--) {
var key = props[fromRight ? length : ++index];
if (iteratee(iterable[key], key, iterable) === false) {
break;
}
}
return object;
};
}
/**
* The base implementation of `baseForOwn` which iterates over `object`
* properties returned by `keysFunc` and invokes `iteratee` for each property.
* Iteratee functions may exit iteration early by explicitly returning `false`.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @param {Function} keysFunc The function to get the keys of `object`.
* @returns {Object} Returns `object`.
*/
var baseFor = createBaseFor();
/**
* The base implementation of `_.forOwn` without support for iteratee shorthands.
*
* @private
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Object} Returns `object`.
*/
function baseForOwn(object, iteratee) {
return object && baseFor(object, iteratee, keys);
}
/**
* The base implementation of `_.findIndex` and `_.findLastIndex` without
* support for iteratee shorthands.
*
* @private
* @param {Array} array The array to inspect.
* @param {Function} predicate The function invoked per iteration.
* @param {number} fromIndex The index to search from.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseFindIndex(array, predicate, fromIndex, fromRight) {
var length = array.length,
index = fromIndex + (fromRight ? 1 : -1);
while ((fromRight ? index-- : ++index < length)) {
if (predicate(array[index], index, array)) {
return index;
}
}
return -1;
}
/**
* The base implementation of `_.isNaN` without support for number objects.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
*/
function baseIsNaN(value) {
return value !== value;
}
/**
* A specialized version of `_.indexOf` which performs strict equality
* comparisons of values, i.e. `===`.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function strictIndexOf(array, value, fromIndex) {
var index = fromIndex - 1,
length = array.length;
while (++index < length) {
if (array[index] === value) {
return index;
}
}
return -1;
}
/**
* The base implementation of `_.indexOf` without `fromIndex` bounds checks.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} fromIndex The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function baseIndexOf(array, value, fromIndex) {
return value === value
? strictIndexOf(array, value, fromIndex)
: baseFindIndex(array, baseIsNaN, fromIndex);
}
/**
* Determines the best order for running the functions in `tasks`, based on
* their requirements. Each function can optionally depend on other functions
* being completed first, and each function is run as soon as its requirements
* are satisfied.
*
* If any of the functions pass an error to their callback, the `auto` sequence
* will stop. Further tasks will not execute (so any other functions depending
* on it will not run), and the main `callback` is immediately called with the
* error.
*
* Functions also receive an object containing the results of functions which
* have completed so far as the first argument, if they have dependencies. If a
* task function has no dependencies, it will only be passed a callback.
*
* @name auto
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Object} tasks - An object. Each of its properties is either a
* function or an array of requirements, with the function itself the last item
* in the array. The object's key of a property serves as the name of the task
* defined by that property, i.e. can be used when specifying requirements for
* other tasks. The function receives one or two arguments:
* * a `results` object, containing the results of the previously executed
* functions, only passed if the task has any dependencies,
* * a `callback(err, result)` function, which must be called when finished,
* passing an `error` (which can be `null`) and the result of the function's
* execution.
* @param {number} [concurrency=Infinity] - An optional `integer` for
* determining the maximum number of tasks that can be run in parallel. By
* default, as many as possible.
* @param {Function} [callback] - An optional callback which is called when all
* the tasks have been completed. It receives the `err` argument if any `tasks`
* pass an error to their callback. Results are always returned; however, if an
* error occurs, no further `tasks` will be performed, and the results object
* will only contain partial results. Invoked with (err, results).
* @returns undefined
* @example
*
* async.auto({
* // this function will just be passed a callback
* readData: async.apply(fs.readFile, 'data.txt', 'utf-8'),
* showData: ['readData', function(results, cb) {
* // results.readData is the file's contents
* // ...
* }]
* }, callback);
*
* async.auto({
* get_data: function(callback) {
* console.log('in get_data');
* // async code to get some data
* callback(null, 'data', 'converted to array');
* },
* make_folder: function(callback) {
* console.log('in make_folder');
* // async code to create a directory to store a file in
* // this is run at the same time as getting the data
* callback(null, 'folder');
* },
* write_file: ['get_data', 'make_folder', function(results, callback) {
* console.log('in write_file', JSON.stringify(results));
* // once there is some data and the directory exists,
* // write the data to a file in the directory
* callback(null, 'filename');
* }],
* email_link: ['write_file', function(results, callback) {
* console.log('in email_link', JSON.stringify(results));
* // once the file is written let's email a link to it...
* // results.write_file contains the filename returned by write_file.
* callback(null, {'file':results.write_file, 'email':'user@example.com'});
* }]
* }, function(err, results) {
* console.log('err = ', err);
* console.log('results = ', results);
* });
*/
var auto = function (tasks, concurrency, callback) {
if (typeof concurrency === 'function') {
// concurrency is optional, shift the args.
callback = concurrency;
concurrency = null;
}
callback = once(callback || noop);
var keys$$1 = keys(tasks);
var numTasks = keys$$1.length;
if (!numTasks) {
return callback(null);
}
if (!concurrency) {
concurrency = numTasks;
}
var results = {};
var runningTasks = 0;
var hasError = false;
var listeners = Object.create(null);
var readyTasks = [];
// for cycle detection:
var readyToCheck = []; // tasks that have been identified as reachable
// without the possibility of returning to an ancestor task
var uncheckedDependencies = {};
baseForOwn(tasks, function (task, key) {
if (!isArray(task)) {
// no dependencies
enqueueTask(key, [task]);
readyToCheck.push(key);
return;
}
var dependencies = task.slice(0, task.length - 1);
var remainingDependencies = dependencies.length;
if (remainingDependencies === 0) {
enqueueTask(key, task);
readyToCheck.push(key);
return;
}
uncheckedDependencies[key] = remainingDependencies;
arrayEach(dependencies, function (dependencyName) {
if (!tasks[dependencyName]) {
throw new Error('async.auto task `' + key + '` has a non-existent dependency `' + dependencyName + '` in ' + dependencies.join(', '));
}
addListener(dependencyName, function () {
remainingDependencies--;
if (remainingDependencies === 0) {
enqueueTask(key, task);
}
});
});
});
checkForDeadlocks();
processQueue();
function enqueueTask(key, task) {
readyTasks.push(function () {
runTask(key, task);
});
}
function processQueue() {
if (readyTasks.length === 0 && runningTasks === 0) {
return callback(null, results);
}
while (readyTasks.length && runningTasks < concurrency) {
var run = readyTasks.shift();
run();
}
}
function addListener(taskName, fn) {
var taskListeners = listeners[taskName];
if (!taskListeners) {
taskListeners = listeners[taskName] = [];
}
taskListeners.push(fn);
}
function taskComplete(taskName) {
var taskListeners = listeners[taskName] || [];
arrayEach(taskListeners, function (fn) {
fn();
});
processQueue();
}
function runTask(key, task) {
if (hasError) return;
var taskCallback = onlyOnce(rest(function (err, args) {
runningTasks--;
if (args.length <= 1) {
args = args[0];
}
if (err) {
var safeResults = {};
baseForOwn(results, function (val, rkey) {
safeResults[rkey] = val;
});
safeResults[key] = args;
hasError = true;
listeners = Object.create(null);
callback(err, safeResults);
} else {
results[key] = args;
taskComplete(key);
}
}));
runningTasks++;
var taskFn = task[task.length - 1];
if (task.length > 1) {
taskFn(results, taskCallback);
} else {
taskFn(taskCallback);
}
}
function checkForDeadlocks() {
// Kahn's algorithm
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
// http://connalle.blogspot.com/2013/10/topological-sortingkahn-algorithm.html
var currentTask;
var counter = 0;
while (readyToCheck.length) {
currentTask = readyToCheck.pop();
counter++;
arrayEach(getDependents(currentTask), function (dependent) {
if (--uncheckedDependencies[dependent] === 0) {
readyToCheck.push(dependent);
}
});
}
if (counter !== numTasks) {
throw new Error('async.auto cannot execute tasks due to a recursive dependency');
}
}
function getDependents(taskName) {
var result = [];
baseForOwn(tasks, function (task, key) {
if (isArray(task) && baseIndexOf(task, taskName, 0) >= 0) {
result.push(key);
}
});
return result;
}
};
/**
* A specialized version of `_.map` for arrays without support for iteratee
* shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the new mapped array.
*/
function arrayMap(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length,
result = Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
/** `Object#toString` result references. */
var symbolTag = '[object Symbol]';
/**
* Checks if `value` is classified as a `Symbol` primitive or object.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
* @example
*
* _.isSymbol(Symbol.iterator);
* // => true
*
* _.isSymbol('abc');
* // => false
*/
function isSymbol(value) {
return typeof value == 'symbol' ||
(isObjectLike(value) && baseGetTag(value) == symbolTag);
}
/** Used as references for various `Number` constants. */
var INFINITY = 1 / 0;
/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol$1 ? Symbol$1.prototype : undefined;
var symbolToString = symbolProto ? symbolProto.toString : undefined;
/**
* The base implementation of `_.toString` which doesn't convert nullish
* values to empty strings.
*
* @private
* @param {*} value The value to process.
* @returns {string} Returns the string.
*/
function baseToString(value) {
// Exit early for strings to avoid a performance hit in some environments.
if (typeof value == 'string') {
return value;
}
if (isArray(value)) {
// Recursively convert values (susceptible to call stack limits).
return arrayMap(value, baseToString) + '';
}
if (isSymbol(value)) {
return symbolToString ? symbolToString.call(value) : '';
}
var result = (value + '');
return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
}
/**
* The base implementation of `_.slice` without an iteratee call guard.
*
* @private
* @param {Array} array The array to slice.
* @param {number} [start=0] The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the slice of `array`.
*/
function baseSlice(array, start, end) {
var index = -1,
length = array.length;
if (start < 0) {
start = -start > length ? 0 : (length + start);
}
end = end > length ? length : end;
if (end < 0) {
end += length;
}
length = start > end ? 0 : ((end - start) >>> 0);
start >>>= 0;
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
/**
* Casts `array` to a slice if it's needed.
*
* @private
* @param {Array} array The array to inspect.
* @param {number} start The start position.
* @param {number} [end=array.length] The end position.
* @returns {Array} Returns the cast slice.
*/
function castSlice(array, start, end) {
var length = array.length;
end = end === undefined ? length : end;
return (!start && end >= length) ? array : baseSlice(array, start, end);
}
/**
* Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol
* that is not found in the character symbols.
*
* @private
* @param {Array} strSymbols The string symbols to inspect.
* @param {Array} chrSymbols The character symbols to find.
* @returns {number} Returns the index of the last unmatched string symbol.
*/
function charsEndIndex(strSymbols, chrSymbols) {
var index = strSymbols.length;
while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
return index;
}
/**
* Used by `_.trim` and `_.trimStart` to get the index of the first string symbol
* that is not found in the character symbols.
*
* @private
* @param {Array} strSymbols The string symbols to inspect.
* @param {Array} chrSymbols The character symbols to find.
* @returns {number} Returns the index of the first unmatched string symbol.
*/
function charsStartIndex(strSymbols, chrSymbols) {
var index = -1,
length = strSymbols.length;
while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
return index;
}
/**
* Converts an ASCII `string` to an array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the converted array.
*/
function asciiToArray(string) {
return string.split('');
}
/** Used to compose unicode character classes. */
var rsAstralRange = '\\ud800-\\udfff';
var rsComboMarksRange = '\\u0300-\\u036f';
var reComboHalfMarksRange = '\\ufe20-\\ufe2f';
var rsComboSymbolsRange = '\\u20d0-\\u20ff';
var rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange;
var rsVarRange = '\\ufe0e\\ufe0f';
/** Used to compose unicode capture groups. */
var rsZWJ = '\\u200d';
/** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */
var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']');
/**
* Checks if `string` contains Unicode symbols.
*
* @private
* @param {string} string The string to inspect.
* @returns {boolean} Returns `true` if a symbol is found, else `false`.
*/
function hasUnicode(string) {
return reHasUnicode.test(string);
}
/** Used to compose unicode character classes. */
var rsAstralRange$1 = '\\ud800-\\udfff';
var rsComboMarksRange$1 = '\\u0300-\\u036f';
var reComboHalfMarksRange$1 = '\\ufe20-\\ufe2f';
var rsComboSymbolsRange$1 = '\\u20d0-\\u20ff';
var rsComboRange$1 = rsComboMarksRange$1 + reComboHalfMarksRange$1 + rsComboSymbolsRange$1;
var rsVarRange$1 = '\\ufe0e\\ufe0f';
/** Used to compose unicode capture groups. */
var rsAstral = '[' + rsAstralRange$1 + ']';
var rsCombo = '[' + rsComboRange$1 + ']';
var rsFitz = '\\ud83c[\\udffb-\\udfff]';
var rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')';
var rsNonAstral = '[^' + rsAstralRange$1 + ']';
var rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}';
var rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]';
var rsZWJ$1 = '\\u200d';
/** Used to compose unicode regexes. */
var reOptMod = rsModifier + '?';
var rsOptVar = '[' + rsVarRange$1 + ']?';
var rsOptJoin = '(?:' + rsZWJ$1 + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*';
var rsSeq = rsOptVar + reOptMod + rsOptJoin;
var rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';
/** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */
var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');
/**
* Converts a Unicode `string` to an array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the converted array.
*/
function unicodeToArray(string) {
return string.match(reUnicode) || [];
}
/**
* Converts `string` to an array.
*
* @private
* @param {string} string The string to convert.
* @returns {Array} Returns the converted array.
*/
function stringToArray(string) {
return hasUnicode(string)
? unicodeToArray(string)
: asciiToArray(string);
}
/**
* Converts `value` to a string. An empty string is returned for `null`
* and `undefined` values. The sign of `-0` is preserved.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
* @example
*
* _.toString(null);
* // => ''
*
* _.toString(-0);
* // => '-0'
*
* _.toString([1, 2, 3]);
* // => '1,2,3'
*/
function toString(value) {
return value == null ? '' : baseToString(value);
}
/** Used to match leading and trailing whitespace. */
var reTrim = /^\s+|\s+$/g;
/**
* Removes leading and trailing whitespace or specified characters from `string`.
*
* @static
* @memberOf _
* @since 3.0.0
* @category String
* @param {string} [string=''] The string to trim.
* @param {string} [chars=whitespace] The characters to trim.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {string} Returns the trimmed string.
* @example
*
* _.trim(' abc ');
* // => 'abc'
*
* _.trim('-_-abc-_-', '_-');
* // => 'abc'
*
* _.map([' foo ', ' bar '], _.trim);
* // => ['foo', 'bar']
*/
function trim(string, chars, guard) {
string = toString(string);
if (string && (guard || chars === undefined)) {
return string.replace(reTrim, '');
}
if (!string || !(chars = baseToString(chars))) {
return string;
}
var strSymbols = stringToArray(string),
chrSymbols = stringToArray(chars),
start = charsStartIndex(strSymbols, chrSymbols),
end = charsEndIndex(strSymbols, chrSymbols) + 1;
return castSlice(strSymbols, start, end).join('');
}
var FN_ARGS = /^(function)?\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /(=.+)?(\s*)$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function parseParams(func) {
func = func.toString().replace(STRIP_COMMENTS, '');
func = func.match(FN_ARGS)[2].replace(' ', '');
func = func ? func.split(FN_ARG_SPLIT) : [];
func = func.map(function (arg) {
return trim(arg.replace(FN_ARG, ''));
});
return func;
}
/**
* A dependency-injected version of the [async.auto]{@link module:ControlFlow.auto} function. Dependent
* tasks are specified as parameters to the function, after the usual callback
* parameter, with the parameter names matching the names of the tasks it
* depends on. This can provide even more readable task graphs which can be
* easier to maintain.
*
* If a final callback is specified, the task results are similarly injected,
* specified as named parameters after the initial error parameter.
*
* The autoInject function is purely syntactic sugar and its semantics are
* otherwise equivalent to [async.auto]{@link module:ControlFlow.auto}.
*
* @name autoInject
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.auto]{@link module:ControlFlow.auto}
* @category Control Flow
* @param {Object} tasks - An object, each of whose properties is a function of
* the form 'func([dependencies...], callback). The object's key of a property
* serves as the name of the task defined by that property, i.e. can be used
* when specifying requirements for other tasks.
* * The `callback` parameter is a `callback(err, result)` which must be called
* when finished, passing an `error` (which can be `null`) and the result of
* the function's execution. The remaining parameters name other tasks on
* which the task is dependent, and the results from those tasks are the
* arguments of those parameters.
* @param {Function} [callback] - An optional callback which is called when all
* the tasks have been completed. It receives the `err` argument if any `tasks`
* pass an error to their callback, and a `results` object with any completed
* task results, similar to `auto`.
* @example
*
* // The example from `auto` can be rewritten as follows:
* async.autoInject({
* get_data: function(callback) {
* // async code to get some data
* callback(null, 'data', 'converted to array');
* },
* make_folder: function(callback) {
* // async code to create a directory to store a file in
* // this is run at the same time as getting the data
* callback(null, 'folder');
* },
* write_file: function(get_data, make_folder, callback) {
* // once there is some data and the directory exists,
* // write the data to a file in the directory
* callback(null, 'filename');
* },
* email_link: function(write_file, callback) {
* // once the file is written let's email a link to it...
* // write_file contains the filename returned by write_file.
* callback(null, {'file':write_file, 'email':'user@example.com'});
* }
* }, function(err, results) {
* console.log('err = ', err);
* console.log('email_link = ', results.email_link);
* });
*
* // If you are using a JS minifier that mangles parameter names, `autoInject`
* // will not work with plain functions, since the parameter names will be
* // collapsed to a single letter identifier. To work around this, you can
* // explicitly specify the names of the parameters your task function needs
* // in an array, similar to Angular.js dependency injection.
*
* // This still has an advantage over plain `auto`, since the results a task
* // depends on are still spread into arguments.
* async.autoInject({
* //...
* write_file: ['get_data', 'make_folder', function(get_data, make_folder, callback) {
* callback(null, 'filename');
* }],
* email_link: ['write_file', function(write_file, callback) {
* callback(null, {'file':write_file, 'email':'user@example.com'});
* }]
* //...
* }, function(err, results) {
* console.log('err = ', err);
* console.log('email_link = ', results.email_link);
* });
*/
function autoInject(tasks, callback) {
var newTasks = {};
baseForOwn(tasks, function (taskFn, key) {
var params;
if (isArray(taskFn)) {
params = taskFn.slice(0, -1);
taskFn = taskFn[taskFn.length - 1];
newTasks[key] = params.concat(params.length > 0 ? newTask : taskFn);
} else if (taskFn.length === 1) {
// no dependencies, use the function as-is
newTasks[key] = taskFn;
} else {
params = parseParams(taskFn);
if (taskFn.length === 0 && params.length === 0) {
throw new Error("autoInject task functions require explicit parameters.");
}
params.pop();
newTasks[key] = params.concat(newTask);
}
function newTask(results, taskCb) {
var newArgs = arrayMap(params, function (name) {
return results[name];
});
newArgs.push(taskCb);
taskFn.apply(null, newArgs);
}
});
auto(newTasks, callback);
}
var hasSetImmediate = typeof setImmediate === 'function' && setImmediate;
var hasNextTick = typeof process === 'object' && typeof process.nextTick === 'function';
function fallback(fn) {
setTimeout(fn, 0);
}
function wrap(defer) {
return rest(function (fn, args) {
defer(function () {
fn.apply(null, args);
});
});
}
var _defer;
Eif (hasSetImmediate) {
_defer = setImmediate;
} else if (hasNextTick) {
_defer = process.nextTick;
} else {
_defer = fallback;
}
var setImmediate$1 = wrap(_defer);
// Simple doubly linked list (https://en.wikipedia.org/wiki/Doubly_linked_list) implementation
// used for queues. This implementation assumes that the node provided by the user can be modified
// to adjust the next and last properties. We implement only the minimal functionality
// for queue support.
function DLL() {
this.head = this.tail = null;
this.length = 0;
}
function setInitial(dll, node) {
dll.length = 1;
dll.head = dll.tail = node;
}
DLL.prototype.removeLink = function (node) {
if (node.prev) node.prev.next = node.next;else this.head = node.next;
if (node.next) node.next.prev = node.prev;else this.tail = node.prev;
node.prev = node.next = null;
this.length -= 1;
return node;
};
DLL.prototype.empty = DLL;
DLL.prototype.insertAfter = function (node, newNode) {
newNode.prev = node;
newNode.next = node.next;
if (node.next) node.next.prev = newNode;else this.tail = newNode;
node.next = newNode;
this.length += 1;
};
DLL.prototype.insertBefore = function (node, newNode) {
newNode.prev = node.prev;
newNode.next = node;
if (node.prev) node.prev.next = newNode;else this.head = newNode;
node.prev = newNode;
this.length += 1;
};
DLL.prototype.unshift = function (node) {
if (this.head) this.insertBefore(this.head, node);else setInitial(this, node);
};
DLL.prototype.push = function (node) {
if (this.tail) this.insertAfter(this.tail, node);else setInitial(this, node);
};
DLL.prototype.shift = function () {
return this.head && this.removeLink(this.head);
};
DLL.prototype.pop = function () {
return this.tail && this.removeLink(this.tail);
};
function queue(worker, concurrency, payload) {
if (concurrency == null) {
concurrency = 1;
} else if (concurrency === 0) {
throw new Error('Concurrency must not be zero');
}
function _insert(data, insertAtFront, callback) {
if (callback != null && typeof callback !== 'function') {
throw new Error('task callback must be a function');
}
q.started = true;
if (!isArray(data)) {
data = [data];
}
if (data.length === 0 && q.idle()) {
// call drain immediately if there are no tasks
return setImmediate$1(function () {
q.drain();
});
}
for (var i = 0, l = data.length; i < l; i++) {
var item = {
data: data[i],
callback: callback || noop
};
if (insertAtFront) {
q._tasks.unshift(item);
} else {
q._tasks.push(item);
}
}
setImmediate$1(q.process);
}
function _next(tasks) {
return rest(function (args) {
workers -= 1;
for (var i = 0, l = tasks.length; i < l; i++) {
var task = tasks[i];
var index = baseIndexOf(workersList, task, 0);
if (index >= 0) {
workersList.splice(index);
}
task.callback.apply(task, args);
if (args[0] != null) {
q.error(args[0], task.data);
}
}
if (workers <= q.concurrency - q.buffer) {
q.unsaturated();
}
if (q.idle()) {
q.drain();
}
q.process();
});
}
var workers = 0;
var workersList = [];
var isProcessing = false;
var q = {
_tasks: new DLL(),
concurrency: concurrency,
payload: payload,
saturated: noop,
unsaturated: noop,
buffer: concurrency / 4,
empty: noop,
drain: noop,
error: noop,
started: false,
paused: false,
push: function (data, callback) {
_insert(data, false, callback);
},
kill: function () {
q.drain = noop;
q._tasks.empty();
},
unshift: function (data, callback) {
_insert(data, true, callback);
},
process: function () {
// Avoid trying to start too many processing operations. This can occur
// when callbacks resolve synchronously (#1267).
if (isProcessing) {
return;
}
isProcessing = true;
while (!q.paused && workers < q.concurrency && q._tasks.length) {
var tasks = [],
data = [];
var l = q._tasks.length;
if (q.payload) l = Math.min(l, q.payload);
for (var i = 0; i < l; i++) {
var node = q._tasks.shift();
tasks.push(node);
data.push(node.data);
}
if (q._tasks.length === 0) {
q.empty();
}
workers += 1;
workersList.push(tasks[0]);
if (workers === q.concurrency) {
q.saturated();
}
var cb = onlyOnce(_next(tasks));
worker(data, cb);
}
isProcessing = false;
},
length: function () {
return q._tasks.length;
},
running: function () {
return workers;
},
workersList: function () {
return workersList;
},
idle: function () {
return q._tasks.length + workers === 0;
},
pause: function () {
q.paused = true;
},
resume: function () {
if (q.paused === false) {
return;
}
q.paused = false;
setImmediate$1(q.process);
}
};
return q;
}
/**
* A cargo of tasks for the worker function to complete. Cargo inherits all of
* the same methods and event callbacks as [`queue`]{@link module:ControlFlow.queue}.
* @typedef {Object} CargoObject
* @memberOf module:ControlFlow
* @property {Function} length - A function returning the number of items
* waiting to be processed. Invoke like `cargo.length()`.
* @property {number} payload - An `integer` for determining how many tasks
* should be process per round. This property can be changed after a `cargo` is
* created to alter the payload on-the-fly.
* @property {Function} push - Adds `task` to the `queue`. The callback is
* called once the `worker` has finished processing the task. Instead of a
* single task, an array of `tasks` can be submitted. The respective callback is
* used for every task in the list. Invoke like `cargo.push(task, [callback])`.
* @property {Function} saturated - A callback that is called when the
* `queue.length()` hits the concurrency and further tasks will be queued.
* @property {Function} empty - A callback that is called when the last item
* from the `queue` is given to a `worker`.
* @property {Function} drain - A callback that is called when the last item
* from the `queue` has returned from the `worker`.
* @property {Function} idle - a function returning false if there are items
* waiting or being processed, or true if not. Invoke like `cargo.idle()`.
* @property {Function} pause - a function that pauses the processing of tasks
* until `resume()` is called. Invoke like `cargo.pause()`.
* @property {Function} resume - a function that resumes the processing of
* queued tasks when the queue is paused. Invoke like `cargo.resume()`.
* @property {Function} kill - a function that removes the `drain` callback and
* empties remaining tasks from the queue forcing it to go idle. Invoke like `cargo.kill()`.
*/
/**
* Creates a `cargo` object with the specified payload. Tasks added to the
* cargo will be processed altogether (up to the `payload` limit). If the
* `worker` is in progress, the task is queued until it becomes available. Once
* the `worker` has completed some tasks, each callback of those tasks is
* called. Check out [these](https://camo.githubusercontent.com/6bbd36f4cf5b35a0f11a96dcd2e97711ffc2fb37/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130382f62626330636662302d356632392d313165322d393734662d3333393763363464633835382e676966) [animations](https://camo.githubusercontent.com/f4810e00e1c5f5f8addbe3e9f49064fd5d102699/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130312f38346339323036362d356632392d313165322d383134662d3964336430323431336266642e676966)
* for how `cargo` and `queue` work.
*
* While [`queue`]{@link module:ControlFlow.queue} passes only one task to one of a group of workers
* at a time, cargo passes an array of tasks to a single worker, repeating
* when the worker is finished.
*
* @name cargo
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.queue]{@link module:ControlFlow.queue}
* @category Control Flow
* @param {Function} worker - An asynchronous function for processing an array
* of queued tasks, which must call its `callback(err)` argument when finished,
* with an optional `err` argument. Invoked with `(tasks, callback)`.
* @param {number} [payload=Infinity] - An optional `integer` for determining
* how many tasks should be processed per round; if omitted, the default is
* unlimited.
* @returns {module:ControlFlow.CargoObject} A cargo object to manage the tasks. Callbacks can
* attached as certain properties to listen for specific events during the
* lifecycle of the cargo and inner queue.
* @example
*
* // create a cargo object with payload 2
* var cargo = async.cargo(function(tasks, callback) {
* for (var i=0; i<tasks.length; i++) {
* console.log('hello ' + tasks[i].name);
* }
* callback();
* }, 2);
*
* // add some items
* cargo.push({name: 'foo'}, function(err) {
* console.log('finished processing foo');
* });
* cargo.push({name: 'bar'}, function(err) {
* console.log('finished processing bar');
* });
* cargo.push({name: 'baz'}, function(err) {
* console.log('finished processing baz');
* });
*/
function cargo(worker, payload) {
return queue(worker, 1, payload);
}
/**
* The same as [`eachOf`]{@link module:Collections.eachOf} but runs only a single async operation at a time.
*
* @name eachOfSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.eachOf]{@link module:Collections.eachOf}
* @alias forEachOfSeries
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item in `coll`. The
* `key` is the item's key, or index in the case of an array. The iteratee is
* passed a `callback(err)` which must be called once it has completed. If no
* error has occurred, the callback should be run without arguments or with an
* explicit `null` argument. Invoked with (item, key, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. Invoked with (err).
*/
var eachOfSeries = doLimit(eachOfLimit, 1);
/**
* Reduces `coll` into a single value using an async `iteratee` to return each
* successive step. `memo` is the initial state of the reduction. This function
* only operates in series.
*
* For performance reasons, it may make sense to split a call to this function
* into a parallel map, and then use the normal `Array.prototype.reduce` on the
* results. This function is for situations where each step in the reduction
* needs to be async; if you can get the data before reducing it, then it's
* probably a good idea to do so.
*
* @name reduce
* @static
* @memberOf module:Collections
* @method
* @alias inject
* @alias foldl
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {*} memo - The initial state of the reduction.
* @param {Function} iteratee - A function applied to each item in the
* array to produce the next step in the reduction. The `iteratee` is passed a
* `callback(err, reduction)` which accepts an optional error as its first
* argument, and the state of the reduction as the second. If an error is
* passed to the callback, the reduction is stopped and the main `callback` is
* immediately called with the error. Invoked with (memo, item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Result is the reduced value. Invoked with
* (err, result).
* @example
*
* async.reduce([1,2,3], 0, function(memo, item, callback) {
* // pointless async:
* process.nextTick(function() {
* callback(null, memo + item)
* });
* }, function(err, result) {
* // result is now equal to the last value of memo, which is 6
* });
*/
function reduce(coll, memo, iteratee, callback) {
callback = once(callback || noop);
eachOfSeries(coll, function (x, i, callback) {
iteratee(memo, x, function (err, v) {
memo = v;
callback(err);
});
}, function (err) {
callback(err, memo);
});
}
/**
* Version of the compose function that is more natural to read. Each function
* consumes the return value of the previous function. It is the equivalent of
* [compose]{@link module:ControlFlow.compose} with the arguments reversed.
*
* Each function is executed with the `this` binding of the composed function.
*
* @name seq
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.compose]{@link module:ControlFlow.compose}
* @category Control Flow
* @param {...Function} functions - the asynchronous functions to compose
* @returns {Function} a function that composes the `functions` in order
* @example
*
* // Requires lodash (or underscore), express3 and dresende's orm2.
* // Part of an app, that fetches cats of the logged user.
* // This example uses `seq` function to avoid overnesting and error
* // handling clutter.
* app.get('/cats', function(request, response) {
* var User = request.models.User;
* async.seq(
* _.bind(User.get, User), // 'User.get' has signature (id, callback(err, data))
* function(user, fn) {
* user.getCats(fn); // 'getCats' has signature (callback(err, data))
* }
* )(req.session.user_id, function (err, cats) {
* if (err) {
* console.error(err);
* response.json({ status: 'error', message: err.message });
* } else {
* response.json({ status: 'ok', message: 'Cats found', data: cats });
* }
* });
* });
*/
var seq$1 = rest(function seq(functions) {
return rest(function (args) {
var that = this;
var cb = args[args.length - 1];
if (typeof cb == 'function') {
args.pop();
} else {
cb = noop;
}
reduce(functions, args, function (newargs, fn, cb) {
fn.apply(that, newargs.concat(rest(function (err, nextargs) {
cb(err, nextargs);
})));
}, function (err, results) {
cb.apply(that, [err].concat(results));
});
});
});
/**
* Creates a function which is a composition of the passed asynchronous
* functions. Each function consumes the return value of the function that
* follows. Composing functions `f()`, `g()`, and `h()` would produce the result
* of `f(g(h()))`, only this version uses callbacks to obtain the return values.
*
* Each function is executed with the `this` binding of the composed function.
*
* @name compose
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {...Function} functions - the asynchronous functions to compose
* @returns {Function} an asynchronous function that is the composed
* asynchronous `functions`
* @example
*
* function add1(n, callback) {
* setTimeout(function () {
* callback(null, n + 1);
* }, 10);
* }
*
* function mul3(n, callback) {
* setTimeout(function () {
* callback(null, n * 3);
* }, 10);
* }
*
* var add1mul3 = async.compose(mul3, add1);
* add1mul3(4, function (err, result) {
* // result now equals 15
* });
*/
var compose = rest(function (args) {
return seq$1.apply(null, args.reverse());
});
function concat$1(eachfn, arr, fn, callback) {
var result = [];
eachfn(arr, function (x, index, cb) {
fn(x, function (err, y) {
result = result.concat(y || []);
cb(err);
});
}, function (err) {
callback(err, result);
});
}
/**
* Applies `iteratee` to each item in `coll`, concatenating the results. Returns
* the concatenated list. The `iteratee`s are called in parallel, and the
* results are concatenated as they return. There is no guarantee that the
* results array will be returned in the original order of `coll` passed to the
* `iteratee` function.
*
* @name concat
* @static
* @memberOf module:Collections
* @method
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item in `coll`.
* The iteratee is passed a `callback(err, results)` which must be called once
* it has completed with an error (which can be `null`) and an array of results.
* Invoked with (item, callback).
* @param {Function} [callback(err)] - A callback which is called after all the
* `iteratee` functions have finished, or an error occurs. Results is an array
* containing the concatenated results of the `iteratee` function. Invoked with
* (err, results).
* @example
*
* async.concat(['dir1','dir2','dir3'], fs.readdir, function(err, files) {
* // files is now a list of filenames that exist in the 3 directories
* });
*/
var concat = doParallel(concat$1);
function doSeries(fn) {
return function (obj, iteratee, callback) {
return fn(eachOfSeries, obj, iteratee, callback);
};
}
/**
* The same as [`concat`]{@link module:Collections.concat} but runs only a single async operation at a time.
*
* @name concatSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.concat]{@link module:Collections.concat}
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item in `coll`.
* The iteratee is passed a `callback(err, results)` which must be called once
* it has completed with an error (which can be `null`) and an array of results.
* Invoked with (item, callback).
* @param {Function} [callback(err)] - A callback which is called after all the
* `iteratee` functions have finished, or an error occurs. Results is an array
* containing the concatenated results of the `iteratee` function. Invoked with
* (err, results).
*/
var concatSeries = doSeries(concat$1);
/**
* Returns a function that when called, calls-back with the values provided.
* Useful as the first function in a [`waterfall`]{@link module:ControlFlow.waterfall}, or for plugging values in to
* [`auto`]{@link module:ControlFlow.auto}.
*
* @name constant
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {...*} arguments... - Any number of arguments to automatically invoke
* callback with.
* @returns {Function} Returns a function that when invoked, automatically
* invokes the callback with the previous given arguments.
* @example
*
* async.waterfall([
* async.constant(42),
* function (value, next) {
* // value === 42
* },
* //...
* ], callback);
*
* async.waterfall([
* async.constant(filename, "utf8"),
* fs.readFile,
* function (fileData, next) {
* //...
* }
* //...
* ], callback);
*
* async.auto({
* hostname: async.constant("https://server.net/"),
* port: findFreePort,
* launchServer: ["hostname", "port", function (options, cb) {
* startServer(options, cb);
* }],
* //...
* }, callback);
*/
var constant = rest(function (values) {
var args = [null].concat(values);
return initialParams(function (ignoredArgs, callback) {
return callback.apply(this, args);
});
});
function _createTester(check, getResult) {
return function (eachfn, arr, iteratee, cb) {
cb = cb || noop;
var testPassed = false;
var testResult;
eachfn(arr, function (value, _, callback) {
iteratee(value, function (err, result) {
if (err) {
callback(err);
} else if (check(result) && !testResult) {
testPassed = true;
testResult = getResult(true, value);
callback(null, breakLoop);
} else {
callback();
}
});
}, function (err) {
if (err) {
cb(err);
} else {
cb(null, testPassed ? testResult : getResult(false));
}
});
};
}
function _findGetResult(v, x) {
return x;
}
/**
* Returns the first value in `coll` that passes an async truth test. The
* `iteratee` is applied in parallel, meaning the first iteratee to return
* `true` will fire the detect `callback` with that result. That means the
* result might not be the first item in the original `coll` (in terms of order)
* that passes the test.
* If order within the original `coll` is important, then look at
* [`detectSeries`]{@link module:Collections.detectSeries}.
*
* @name detect
* @static
* @memberOf module:Collections
* @method
* @alias find
* @category Collections
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The iteratee is passed a `callback(err, truthValue)` which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called as soon as any
* iteratee returns `true`, or after all the `iteratee` functions have finished.
* Result will be the first item in the array that passes the truth test
* (iteratee) or the value `undefined` if none passed. Invoked with
* (err, result).
* @example
*
* async.detect(['file1','file2','file3'], function(filePath, callback) {
* fs.access(filePath, function(err) {
* callback(null, !err)
* });
* }, function(err, result) {
* // result now equals the first file in the list that exists
* });
*/
var detect = doParallel(_createTester(identity, _findGetResult));
/**
* The same as [`detect`]{@link module:Collections.detect} but runs a maximum of `limit` async operations at a
* time.
*
* @name detectLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.detect]{@link module:Collections.detect}
* @alias findLimit
* @category Collections
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The iteratee is passed a `callback(err, truthValue)` which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called as soon as any
* iteratee returns `true`, or after all the `iteratee` functions have finished.
* Result will be the first item in the array that passes the truth test
* (iteratee) or the value `undefined` if none passed. Invoked with
* (err, result).
*/
var detectLimit = doParallelLimit(_createTester(identity, _findGetResult));
/**
* The same as [`detect`]{@link module:Collections.detect} but runs only a single async operation at a time.
*
* @name detectSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.detect]{@link module:Collections.detect}
* @alias findSeries
* @category Collections
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The iteratee is passed a `callback(err, truthValue)` which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called as soon as any
* iteratee returns `true`, or after all the `iteratee` functions have finished.
* Result will be the first item in the array that passes the truth test
* (iteratee) or the value `undefined` if none passed. Invoked with
* (err, result).
*/
var detectSeries = doLimit(detectLimit, 1);
function consoleFunc(name) {
return rest(function (fn, args) {
fn.apply(null, args.concat(rest(function (err, args) {
if (typeof console === 'object') {
if (err) {
if (console.error) {
console.error(err);
}
} else if (console[name]) {
arrayEach(args, function (x) {
console[name](x);
});
}
}
})));
});
}
/**
* Logs the result of an `async` function to the `console` using `console.dir`
* to display the properties of the resulting object. Only works in Node.js or
* in browsers that support `console.dir` and `console.error` (such as FF and
* Chrome). If multiple arguments are returned from the async function,
* `console.dir` is called on each argument in order.
*
* @name dir
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} function - The function you want to eventually apply all
* arguments to.
* @param {...*} arguments... - Any number of arguments to apply to the function.
* @example
*
* // in a module
* var hello = function(name, callback) {
* setTimeout(function() {
* callback(null, {hello: name});
* }, 1000);
* };
*
* // in the node repl
* node> async.dir(hello, 'world');
* {hello: 'world'}
*/
var dir = consoleFunc('dir');
/**
* The post-check version of [`during`]{@link module:ControlFlow.during}. To reflect the difference in
* the order of operations, the arguments `test` and `fn` are switched.
*
* Also a version of [`doWhilst`]{@link module:ControlFlow.doWhilst} with asynchronous `test` function.
* @name doDuring
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.during]{@link module:ControlFlow.during}
* @category Control Flow
* @param {Function} fn - A function which is called each time `test` passes.
* The function is passed a `callback(err)`, which must be called once it has
* completed with an optional `err` argument. Invoked with (callback).
* @param {Function} test - asynchronous truth test to perform before each
* execution of `fn`. Invoked with (...args, callback), where `...args` are the
* non-error args from the previous callback of `fn`.
* @param {Function} [callback] - A callback which is called after the test
* function has failed and repeated execution of `fn` has stopped. `callback`
* will be passed an error if one occured, otherwise `null`.
*/
function doDuring(fn, test, callback) {
callback = onlyOnce(callback || noop);
var next = rest(function (err, args) {
if (err) return callback(err);
args.push(check);
test.apply(this, args);
});
function check(err, truth) {
if (err) return callback(err);
if (!truth) return callback(null);
fn(next);
}
check(null, true);
}
/**
* The post-check version of [`whilst`]{@link module:ControlFlow.whilst}. To reflect the difference in
* the order of operations, the arguments `test` and `iteratee` are switched.
*
* `doWhilst` is to `whilst` as `do while` is to `while` in plain JavaScript.
*
* @name doWhilst
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.whilst]{@link module:ControlFlow.whilst}
* @category Control Flow
* @param {Function} iteratee - A function which is called each time `test`
* passes. The function is passed a `callback(err)`, which must be called once
* it has completed with an optional `err` argument. Invoked with (callback).
* @param {Function} test - synchronous truth test to perform after each
* execution of `iteratee`. Invoked with the non-error callback results of
* `iteratee`.
* @param {Function} [callback] - A callback which is called after the test
* function has failed and repeated execution of `iteratee` has stopped.
* `callback` will be passed an error and any arguments passed to the final
* `iteratee`'s callback. Invoked with (err, [results]);
*/
function doWhilst(iteratee, test, callback) {
callback = onlyOnce(callback || noop);
var next = rest(function (err, args) {
if (err) return callback(err);
if (test.apply(this, args)) return iteratee(next);
callback.apply(null, [null].concat(args));
});
iteratee(next);
}
/**
* Like ['doWhilst']{@link module:ControlFlow.doWhilst}, except the `test` is inverted. Note the
* argument ordering differs from `until`.
*
* @name doUntil
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.doWhilst]{@link module:ControlFlow.doWhilst}
* @category Control Flow
* @param {Function} fn - A function which is called each time `test` fails.
* The function is passed a `callback(err)`, which must be called once it has
* completed with an optional `err` argument. Invoked with (callback).
* @param {Function} test - synchronous truth test to perform after each
* execution of `fn`. Invoked with the non-error callback results of `fn`.
* @param {Function} [callback] - A callback which is called after the test
* function has passed and repeated execution of `fn` has stopped. `callback`
* will be passed an error and any arguments passed to the final `fn`'s
* callback. Invoked with (err, [results]);
*/
function doUntil(fn, test, callback) {
doWhilst(fn, function () {
return !test.apply(this, arguments);
}, callback);
}
/**
* Like [`whilst`]{@link module:ControlFlow.whilst}, except the `test` is an asynchronous function that
* is passed a callback in the form of `function (err, truth)`. If error is
* passed to `test` or `fn`, the main callback is immediately called with the
* value of the error.
*
* @name during
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.whilst]{@link module:ControlFlow.whilst}
* @category Control Flow
* @param {Function} test - asynchronous truth test to perform before each
* execution of `fn`. Invoked with (callback).
* @param {Function} fn - A function which is called each time `test` passes.
* The function is passed a `callback(err)`, which must be called once it has
* completed with an optional `err` argument. Invoked with (callback).
* @param {Function} [callback] - A callback which is called after the test
* function has failed and repeated execution of `fn` has stopped. `callback`
* will be passed an error, if one occured, otherwise `null`.
* @example
*
* var count = 0;
*
* async.during(
* function (callback) {
* return callback(null, count < 5);
* },
* function (callback) {
* count++;
* setTimeout(callback, 1000);
* },
* function (err) {
* // 5 seconds have passed
* }
* );
*/
function during(test, fn, callback) {
callback = onlyOnce(callback || noop);
function next(err) {
if (err) return callback(err);
test(check);
}
function check(err, truth) {
if (err) return callback(err);
if (!truth) return callback(null);
fn(next);
}
test(check);
}
function _withoutIndex(iteratee) {
return function (value, index, callback) {
return iteratee(value, callback);
};
}
/**
* Applies the function `iteratee` to each item in `coll`, in parallel.
* The `iteratee` is called with an item from the list, and a callback for when
* it has finished. If the `iteratee` passes an error to its `callback`, the
* main `callback` (for the `each` function) is immediately called with the
* error.
*
* Note, that since this function applies `iteratee` to each item in parallel,
* there is no guarantee that the iteratee functions will complete in order.
*
* @name each
* @static
* @memberOf module:Collections
* @method
* @alias forEach
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item
* in `coll`. The iteratee is passed a `callback(err)` which must be called once
* it has completed. If no error has occurred, the `callback` should be run
* without arguments or with an explicit `null` argument. The array index is not
* passed to the iteratee. Invoked with (item, callback). If you need the index,
* use `eachOf`.
* @param {Function} [callback] - A callback which is called when all
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
* @example
*
* // assuming openFiles is an array of file names and saveFile is a function
* // to save the modified contents of that file:
*
* async.each(openFiles, saveFile, function(err){
* // if any of the saves produced an error, err would equal that error
* });
*
* // assuming openFiles is an array of file names
* async.each(openFiles, function(file, callback) {
*
* // Perform operation on file here.
* console.log('Processing file ' + file);
*
* if( file.length > 32 ) {
* console.log('This file name is too long');
* callback('File name too long');
* } else {
* // Do work to process file here
* console.log('File processed');
* callback();
* }
* }, function(err) {
* // if any of the file processing produced an error, err would equal that error
* if( err ) {
* // One of the iterations produced an error.
* // All processing will now stop.
* console.log('A file failed to process');
* } else {
* console.log('All files have been processed successfully');
* }
* });
*/
function eachLimit(coll, iteratee, callback) {
eachOf(coll, _withoutIndex(iteratee), callback);
}
/**
* The same as [`each`]{@link module:Collections.each} but runs a maximum of `limit` async operations at a time.
*
* @name eachLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.each]{@link module:Collections.each}
* @alias forEachLimit
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A function to apply to each item in `coll`. The
* iteratee is passed a `callback(err)` which must be called once it has
* completed. If no error has occurred, the `callback` should be run without
* arguments or with an explicit `null` argument. The array index is not passed
* to the iteratee. Invoked with (item, callback). If you need the index, use
* `eachOfLimit`.
* @param {Function} [callback] - A callback which is called when all
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
*/
function eachLimit$1(coll, limit, iteratee, callback) {
_eachOfLimit(limit)(coll, _withoutIndex(iteratee), callback);
}
/**
* The same as [`each`]{@link module:Collections.each} but runs only a single async operation at a time.
*
* @name eachSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.each]{@link module:Collections.each}
* @alias forEachSeries
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each
* item in `coll`. The iteratee is passed a `callback(err)` which must be called
* once it has completed. If no error has occurred, the `callback` should be run
* without arguments or with an explicit `null` argument. The array index is
* not passed to the iteratee. Invoked with (item, callback). If you need the
* index, use `eachOfSeries`.
* @param {Function} [callback] - A callback which is called when all
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
*/
var eachSeries = doLimit(eachLimit$1, 1);
/**
* Wrap an async function and ensure it calls its callback on a later tick of
* the event loop. If the function already calls its callback on a next tick,
* no extra deferral is added. This is useful for preventing stack overflows
* (`RangeError: Maximum call stack size exceeded`) and generally keeping
* [Zalgo](http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony)
* contained.
*
* @name ensureAsync
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} fn - an async function, one that expects a node-style
* callback as its last argument.
* @returns {Function} Returns a wrapped function with the exact same call
* signature as the function passed in.
* @example
*
* function sometimesAsync(arg, callback) {
* if (cache[arg]) {
* return callback(null, cache[arg]); // this would be synchronous!!
* } else {
* doSomeIO(arg, callback); // this IO would be asynchronous
* }
* }
*
* // this has a risk of stack overflows if many results are cached in a row
* async.mapSeries(args, sometimesAsync, done);
*
* // this will defer sometimesAsync's callback if necessary,
* // preventing stack overflows
* async.mapSeries(args, async.ensureAsync(sometimesAsync), done);
*/
function ensureAsync(fn) {
return initialParams(function (args, callback) {
var sync = true;
args.push(function () {
var innerArgs = arguments;
if (sync) {
setImmediate$1(function () {
callback.apply(null, innerArgs);
});
} else {
callback.apply(null, innerArgs);
}
});
fn.apply(this, args);
sync = false;
});
}
function notId(v) {
return !v;
}
/**
* Returns `true` if every element in `coll` satisfies an async test. If any
* iteratee call returns `false`, the main `callback` is immediately called.
*
* @name every
* @static
* @memberOf module:Collections
* @method
* @alias all
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in the
* collection in parallel. The iteratee is passed a `callback(err, truthValue)`
* which must be called with a boolean argument once it has completed. Invoked
* with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Result will be either `true` or `false`
* depending on the values of the async tests. Invoked with (err, result).
* @example
*
* async.every(['file1','file2','file3'], function(filePath, callback) {
* fs.access(filePath, function(err) {
* callback(null, !err)
* });
* }, function(err, result) {
* // if result is true then every file exists
* });
*/
var every = doParallel(_createTester(notId, notId));
/**
* The same as [`every`]{@link module:Collections.every} but runs a maximum of `limit` async operations at a time.
*
* @name everyLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.every]{@link module:Collections.every}
* @alias allLimit
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A truth test to apply to each item in the
* collection in parallel. The iteratee is passed a `callback(err, truthValue)`
* which must be called with a boolean argument once it has completed. Invoked
* with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Result will be either `true` or `false`
* depending on the values of the async tests. Invoked with (err, result).
*/
var everyLimit = doParallelLimit(_createTester(notId, notId));
/**
* The same as [`every`]{@link module:Collections.every} but runs only a single async operation at a time.
*
* @name everySeries
* @static
* @memberOf module:Collections
* @method
* @see [async.every]{@link module:Collections.every}
* @alias allSeries
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in the
* collection in parallel. The iteratee is passed a `callback(err, truthValue)`
* which must be called with a boolean argument once it has completed. Invoked
* with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Result will be either `true` or `false`
* depending on the values of the async tests. Invoked with (err, result).
*/
var everySeries = doLimit(everyLimit, 1);
/**
* The base implementation of `_.property` without support for deep paths.
*
* @private
* @param {string} key The key of the property to get.
* @returns {Function} Returns the new accessor function.
*/
function baseProperty(key) {
return function(object) {
return object == null ? undefined : object[key];
};
}
function filterArray(eachfn, arr, iteratee, callback) {
var truthValues = new Array(arr.length);
eachfn(arr, function (x, index, callback) {
iteratee(x, function (err, v) {
truthValues[index] = !!v;
callback(err);
});
}, function (err) {
if (err) return callback(err);
var results = [];
for (var i = 0; i < arr.length; i++) {
if (truthValues[i]) results.push(arr[i]);
}
callback(null, results);
});
}
function filterGeneric(eachfn, coll, iteratee, callback) {
var results = [];
eachfn(coll, function (x, index, callback) {
iteratee(x, function (err, v) {
if (err) {
callback(err);
} else {
if (v) {
results.push({ index: index, value: x });
}
callback();
}
});
}, function (err) {
if (err) {
callback(err);
} else {
callback(null, arrayMap(results.sort(function (a, b) {
return a.index - b.index;
}), baseProperty('value')));
}
});
}
function _filter(eachfn, coll, iteratee, callback) {
var filter = isArrayLike(coll) ? filterArray : filterGeneric;
filter(eachfn, coll, iteratee, callback || noop);
}
/**
* Returns a new array of all the values in `coll` which pass an async truth
* test. This operation is performed in parallel, but the results array will be
* in the same order as the original.
*
* @name filter
* @static
* @memberOf module:Collections
* @method
* @alias select
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The `iteratee` is passed a `callback(err, truthValue)`, which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Invoked with (err, results).
* @example
*
* async.filter(['file1','file2','file3'], function(filePath, callback) {
* fs.access(filePath, function(err) {
* callback(null, !err)
* });
* }, function(err, results) {
* // results now equals an array of the existing files
* });
*/
var filter = doParallel(_filter);
/**
* The same as [`filter`]{@link module:Collections.filter} but runs a maximum of `limit` async operations at a
* time.
*
* @name filterLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.filter]{@link module:Collections.filter}
* @alias selectLimit
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The `iteratee` is passed a `callback(err, truthValue)`, which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Invoked with (err, results).
*/
var filterLimit = doParallelLimit(_filter);
/**
* The same as [`filter`]{@link module:Collections.filter} but runs only a single async operation at a time.
*
* @name filterSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.filter]{@link module:Collections.filter}
* @alias selectSeries
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The `iteratee` is passed a `callback(err, truthValue)`, which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Invoked with (err, results)
*/
var filterSeries = doLimit(filterLimit, 1);
/**
* Calls the asynchronous function `fn` with a callback parameter that allows it
* to call itself again, in series, indefinitely.
* If an error is passed to the
* callback then `errback` is called with the error, and execution stops,
* otherwise it will never be called.
*
* @name forever
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Function} fn - a function to call repeatedly. Invoked with (next).
* @param {Function} [errback] - when `fn` passes an error to it's callback,
* this function will be called, and execution stops. Invoked with (err).
* @example
*
* async.forever(
* function(next) {
* // next is suitable for passing to things that need a callback(err [, whatever]);
* // it will result in this function being called again.
* },
* function(err) {
* // if next is called with a value in its first parameter, it will appear
* // in here as 'err', and execution will stop.
* }
* );
*/
function forever(fn, errback) {
var done = onlyOnce(errback || noop);
var task = ensureAsync(fn);
function next(err) {
if (err) return done(err);
task(next);
}
next();
}
/**
* Logs the result of an `async` function to the `console`. Only works in
* Node.js or in browsers that support `console.log` and `console.error` (such
* as FF and Chrome). If multiple arguments are returned from the async
* function, `console.log` is called on each argument in order.
*
* @name log
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} function - The function you want to eventually apply all
* arguments to.
* @param {...*} arguments... - Any number of arguments to apply to the function.
* @example
*
* // in a module
* var hello = function(name, callback) {
* setTimeout(function() {
* callback(null, 'hello ' + name);
* }, 1000);
* };
*
* // in the node repl
* node> async.log(hello, 'world');
* 'hello world'
*/
var log = consoleFunc('log');
/**
* The same as [`mapValues`]{@link module:Collections.mapValues} but runs a maximum of `limit` async operations at a
* time.
*
* @name mapValuesLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.mapValues]{@link module:Collections.mapValues}
* @category Collection
* @param {Object} obj - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A function to apply to each value in `obj`.
* The iteratee is passed a `callback(err, transformed)` which must be called
* once it has completed with an error (which can be `null`) and a
* transformed value. Invoked with (value, key, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. `result` is a new object consisting
* of each key from `obj`, with each transformed value on the right-hand side.
* Invoked with (err, result).
*/
function mapValuesLimit(obj, limit, iteratee, callback) {
callback = once(callback || noop);
var newObj = {};
eachOfLimit(obj, limit, function (val, key, next) {
iteratee(val, key, function (err, result) {
if (err) return next(err);
newObj[key] = result;
next();
});
}, function (err) {
callback(err, newObj);
});
}
/**
* A relative of [`map`]{@link module:Collections.map}, designed for use with objects.
*
* Produces a new Object by mapping each value of `obj` through the `iteratee`
* function. The `iteratee` is called each `value` and `key` from `obj` and a
* callback for when it has finished processing. Each of these callbacks takes
* two arguments: an `error`, and the transformed item from `obj`. If `iteratee`
* passes an error to its callback, the main `callback` (for the `mapValues`
* function) is immediately called with the error.
*
* Note, the order of the keys in the result is not guaranteed. The keys will
* be roughly in the order they complete, (but this is very engine-specific)
*
* @name mapValues
* @static
* @memberOf module:Collections
* @method
* @category Collection
* @param {Object} obj - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each value and key in
* `coll`. The iteratee is passed a `callback(err, transformed)` which must be
* called once it has completed with an error (which can be `null`) and a
* transformed value. Invoked with (value, key, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. `result` is a new object consisting
* of each key from `obj`, with each transformed value on the right-hand side.
* Invoked with (err, result).
* @example
*
* async.mapValues({
* f1: 'file1',
* f2: 'file2',
* f3: 'file3'
* }, function (file, key, callback) {
* fs.stat(file, callback);
* }, function(err, result) {
* // result is now a map of stats for each file, e.g.
* // {
* // f1: [stats for file1],
* // f2: [stats for file2],
* // f3: [stats for file3]
* // }
* });
*/
var mapValues = doLimit(mapValuesLimit, Infinity);
/**
* The same as [`mapValues`]{@link module:Collections.mapValues} but runs only a single async operation at a time.
*
* @name mapValuesSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.mapValues]{@link module:Collections.mapValues}
* @category Collection
* @param {Object} obj - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each value in `obj`.
* The iteratee is passed a `callback(err, transformed)` which must be called
* once it has completed with an error (which can be `null`) and a
* transformed value. Invoked with (value, key, callback).
* @param {Function} [callback] - A callback which is called when all `iteratee`
* functions have finished, or an error occurs. `result` is a new object consisting
* of each key from `obj`, with each transformed value on the right-hand side.
* Invoked with (err, result).
*/
var mapValuesSeries = doLimit(mapValuesLimit, 1);
function has(obj, key) {
return key in obj;
}
/**
* Caches the results of an `async` function. When creating a hash to store
* function results against, the callback is omitted from the hash and an
* optional hash function can be used.
*
* If no hash function is specified, the first argument is used as a hash key,
* which may work reasonably if it is a string or a data type that converts to a
* distinct string. Note that objects and arrays will not behave reasonably.
* Neither will cases where the other arguments are significant. In such cases,
* specify your own hash function.
*
* The cache of results is exposed as the `memo` property of the function
* returned by `memoize`.
*
* @name memoize
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} fn - The function to proxy and cache results from.
* @param {Function} hasher - An optional function for generating a custom hash
* for storing results. It has all the arguments applied to it apart from the
* callback, and must be synchronous.
* @returns {Function} a memoized version of `fn`
* @example
*
* var slow_fn = function(name, callback) {
* // do something
* callback(null, result);
* };
* var fn = async.memoize(slow_fn);
*
* // fn can now be used as if it were slow_fn
* fn('some name', function() {
* // callback
* });
*/
function memoize(fn, hasher) {
var memo = Object.create(null);
var queues = Object.create(null);
hasher = hasher || identity;
var memoized = initialParams(function memoized(args, callback) {
var key = hasher.apply(null, args);
if (has(memo, key)) {
setImmediate$1(function () {
callback.apply(null, memo[key]);
});
} else if (has(queues, key)) {
queues[key].push(callback);
} else {
queues[key] = [callback];
fn.apply(null, args.concat(rest(function (args) {
memo[key] = args;
var q = queues[key];
delete queues[key];
for (var i = 0, l = q.length; i < l; i++) {
q[i].apply(null, args);
}
})));
}
});
memoized.memo = memo;
memoized.unmemoized = fn;
return memoized;
}
/**
* Calls `callback` on a later loop around the event loop. In Node.js this just
* calls `setImmediate`. In the browser it will use `setImmediate` if
* available, otherwise `setTimeout(callback, 0)`, which means other higher
* priority events may precede the execution of `callback`.
*
* This is used internally for browser-compatibility purposes.
*
* @name nextTick
* @static
* @memberOf module:Utils
* @method
* @alias setImmediate
* @category Util
* @param {Function} callback - The function to call on a later loop around
* the event loop. Invoked with (args...).
* @param {...*} args... - any number of additional arguments to pass to the
* callback on the next tick.
* @example
*
* var call_order = [];
* async.nextTick(function() {
* call_order.push('two');
* // call_order now equals ['one','two']
* });
* call_order.push('one');
*
* async.setImmediate(function (a, b, c) {
* // a, b, and c equal 1, 2, and 3
* }, 1, 2, 3);
*/
var _defer$1;
Eif (hasNextTick) {
_defer$1 = process.nextTick;
} else if (hasSetImmediate) {
_defer$1 = setImmediate;
} else {
_defer$1 = fallback;
}
var nextTick = wrap(_defer$1);
function _parallel(eachfn, tasks, callback) {
callback = callback || noop;
var results = isArrayLike(tasks) ? [] : {};
eachfn(tasks, function (task, key, callback) {
task(rest(function (err, args) {
if (args.length <= 1) {
args = args[0];
}
results[key] = args;
callback(err);
}));
}, function (err) {
callback(err, results);
});
}
/**
* Run the `tasks` collection of functions in parallel, without waiting until
* the previous function has completed. If any of the functions pass an error to
* its callback, the main `callback` is immediately called with the value of the
* error. Once the `tasks` have completed, the results are passed to the final
* `callback` as an array.
*
* **Note:** `parallel` is about kicking-off I/O tasks in parallel, not about
* parallel execution of code. If your tasks do not use any timers or perform
* any I/O, they will actually be executed in series. Any synchronous setup
* sections for each task will happen one after the other. JavaScript remains
* single-threaded.
*
* It is also possible to use an object instead of an array. Each property will
* be run as a function and the results will be passed to the final `callback`
* as an object instead of an array. This can be a more readable way of handling
* results from {@link async.parallel}.
*
* @name parallel
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Array|Iterable|Object} tasks - A collection containing functions to run.
* Each function is passed a `callback(err, result)` which it must call on
* completion with an error `err` (which can be `null`) and an optional `result`
* value.
* @param {Function} [callback] - An optional callback to run once all the
* functions have completed successfully. This function gets a results array
* (or object) containing all the result arguments passed to the task callbacks.
* Invoked with (err, results).
* @example
* async.parallel([
* function(callback) {
* setTimeout(function() {
* callback(null, 'one');
* }, 200);
* },
* function(callback) {
* setTimeout(function() {
* callback(null, 'two');
* }, 100);
* }
* ],
* // optional callback
* function(err, results) {
* // the results array will equal ['one','two'] even though
* // the second function had a shorter timeout.
* });
*
* // an example using an object instead of an array
* async.parallel({
* one: function(callback) {
* setTimeout(function() {
* callback(null, 1);
* }, 200);
* },
* two: function(callback) {
* setTimeout(function() {
* callback(null, 2);
* }, 100);
* }
* }, function(err, results) {
* // results is now equals to: {one: 1, two: 2}
* });
*/
function parallelLimit(tasks, callback) {
_parallel(eachOf, tasks, callback);
}
/**
* The same as [`parallel`]{@link module:ControlFlow.parallel} but runs a maximum of `limit` async operations at a
* time.
*
* @name parallelLimit
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.parallel]{@link module:ControlFlow.parallel}
* @category Control Flow
* @param {Array|Collection} tasks - A collection containing functions to run.
* Each function is passed a `callback(err, result)` which it must call on
* completion with an error `err` (which can be `null`) and an optional `result`
* value.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} [callback] - An optional callback to run once all the
* functions have completed successfully. This function gets a results array
* (or object) containing all the result arguments passed to the task callbacks.
* Invoked with (err, results).
*/
function parallelLimit$1(tasks, limit, callback) {
_parallel(_eachOfLimit(limit), tasks, callback);
}
/**
* A queue of tasks for the worker function to complete.
* @typedef {Object} QueueObject
* @memberOf module:ControlFlow
* @property {Function} length - a function returning the number of items
* waiting to be processed. Invoke with `queue.length()`.
* @property {boolean} started - a boolean indicating whether or not any
* items have been pushed and processed by the queue.
* @property {Function} running - a function returning the number of items
* currently being processed. Invoke with `queue.running()`.
* @property {Function} workersList - a function returning the array of items
* currently being processed. Invoke with `queue.workersList()`.
* @property {Function} idle - a function returning false if there are items
* waiting or being processed, or true if not. Invoke with `queue.idle()`.
* @property {number} concurrency - an integer for determining how many `worker`
* functions should be run in parallel. This property can be changed after a
* `queue` is created to alter the concurrency on-the-fly.
* @property {Function} push - add a new task to the `queue`. Calls `callback`
* once the `worker` has finished processing the task. Instead of a single task,
* a `tasks` array can be submitted. The respective callback is used for every
* task in the list. Invoke with `queue.push(task, [callback])`,
* @property {Function} unshift - add a new task to the front of the `queue`.
* Invoke with `queue.unshift(task, [callback])`.
* @property {Function} saturated - a callback that is called when the number of
* running workers hits the `concurrency` limit, and further tasks will be
* queued.
* @property {Function} unsaturated - a callback that is called when the number
* of running workers is less than the `concurrency` & `buffer` limits, and
* further tasks will not be queued.
* @property {number} buffer - A minimum threshold buffer in order to say that
* the `queue` is `unsaturated`.
* @property {Function} empty - a callback that is called when the last item
* from the `queue` is given to a `worker`.
* @property {Function} drain - a callback that is called when the last item
* from the `queue` has returned from the `worker`.
* @property {Function} error - a callback that is called when a task errors.
* Has the signature `function(error, task)`.
* @property {boolean} paused - a boolean for determining whether the queue is
* in a paused state.
* @property {Function} pause - a function that pauses the processing of tasks
* until `resume()` is called. Invoke with `queue.pause()`.
* @property {Function} resume - a function that resumes the processing of
* queued tasks when the queue is paused. Invoke with `queue.resume()`.
* @property {Function} kill - a function that removes the `drain` callback and
* empties remaining tasks from the queue forcing it to go idle. Invoke with `queue.kill()`.
*/
/**
* Creates a `queue` object with the specified `concurrency`. Tasks added to the
* `queue` are processed in parallel (up to the `concurrency` limit). If all
* `worker`s are in progress, the task is queued until one becomes available.
* Once a `worker` completes a `task`, that `task`'s callback is called.
*
* @name queue
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Function} worker - An asynchronous function for processing a queued
* task, which must call its `callback(err)` argument when finished, with an
* optional `error` as an argument. If you want to handle errors from an
* individual task, pass a callback to `q.push()`. Invoked with
* (task, callback).
* @param {number} [concurrency=1] - An `integer` for determining how many
* `worker` functions should be run in parallel. If omitted, the concurrency
* defaults to `1`. If the concurrency is `0`, an error is thrown.
* @returns {module:ControlFlow.QueueObject} A queue object to manage the tasks. Callbacks can
* attached as certain properties to listen for specific events during the
* lifecycle of the queue.
* @example
*
* // create a queue object with concurrency 2
* var q = async.queue(function(task, callback) {
* console.log('hello ' + task.name);
* callback();
* }, 2);
*
* // assign a callback
* q.drain = function() {
* console.log('all items have been processed');
* };
*
* // add some items to the queue
* q.push({name: 'foo'}, function(err) {
* console.log('finished processing foo');
* });
* q.push({name: 'bar'}, function (err) {
* console.log('finished processing bar');
* });
*
* // add some items to the queue (batch-wise)
* q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function(err) {
* console.log('finished processing item');
* });
*
* // add some items to the front of the queue
* q.unshift({name: 'bar'}, function (err) {
* console.log('finished processing bar');
* });
*/
var queue$1 = function (worker, concurrency) {
return queue(function (items, cb) {
worker(items[0], cb);
}, concurrency, 1);
};
/**
* The same as [async.queue]{@link module:ControlFlow.queue} only tasks are assigned a priority and
* completed in ascending priority order.
*
* @name priorityQueue
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.queue]{@link module:ControlFlow.queue}
* @category Control Flow
* @param {Function} worker - An asynchronous function for processing a queued
* task, which must call its `callback(err)` argument when finished, with an
* optional `error` as an argument. If you want to handle errors from an
* individual task, pass a callback to `q.push()`. Invoked with
* (task, callback).
* @param {number} concurrency - An `integer` for determining how many `worker`
* functions should be run in parallel. If omitted, the concurrency defaults to
* `1`. If the concurrency is `0`, an error is thrown.
* @returns {module:ControlFlow.QueueObject} A priorityQueue object to manage the tasks. There are two
* differences between `queue` and `priorityQueue` objects:
* * `push(task, priority, [callback])` - `priority` should be a number. If an
* array of `tasks` is given, all tasks will be assigned the same priority.
* * The `unshift` method was removed.
*/
var priorityQueue = function (worker, concurrency) {
// Start with a normal queue
var q = queue$1(worker, concurrency);
// Override push to accept second parameter representing priority
q.push = function (data, priority, callback) {
if (callback == null) callback = noop;
if (typeof callback !== 'function') {
throw new Error('task callback must be a function');
}
q.started = true;
if (!isArray(data)) {
data = [data];
}
if (data.length === 0) {
// call drain immediately if there are no tasks
return setImmediate$1(function () {
q.drain();
});
}
priority = priority || 0;
var nextNode = q._tasks.head;
while (nextNode && priority >= nextNode.priority) {
nextNode = nextNode.next;
}
for (var i = 0, l = data.length; i < l; i++) {
var item = {
data: data[i],
priority: priority,
callback: callback
};
if (nextNode) {
q._tasks.insertBefore(nextNode, item);
} else {
q._tasks.push(item);
}
}
setImmediate$1(q.process);
};
// Remove unshift function
delete q.unshift;
return q;
};
/**
* Runs the `tasks` array of functions in parallel, without waiting until the
* previous function has completed. Once any of the `tasks` complete or pass an
* error to its callback, the main `callback` is immediately called. It's
* equivalent to `Promise.race()`.
*
* @name race
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Array} tasks - An array containing functions to run. Each function
* is passed a `callback(err, result)` which it must call on completion with an
* error `err` (which can be `null`) and an optional `result` value.
* @param {Function} callback - A callback to run once any of the functions have
* completed. This function gets an error or result from the first function that
* completed. Invoked with (err, result).
* @returns undefined
* @example
*
* async.race([
* function(callback) {
* setTimeout(function() {
* callback(null, 'one');
* }, 200);
* },
* function(callback) {
* setTimeout(function() {
* callback(null, 'two');
* }, 100);
* }
* ],
* // main callback
* function(err, result) {
* // the result will be equal to 'two' as it finishes earlier
* });
*/
function race(tasks, callback) {
callback = once(callback || noop);
if (!isArray(tasks)) return callback(new TypeError('First argument to race must be an array of functions'));
if (!tasks.length) return callback();
for (var i = 0, l = tasks.length; i < l; i++) {
tasks[i](callback);
}
}
var slice = Array.prototype.slice;
/**
* Same as [`reduce`]{@link module:Collections.reduce}, only operates on `array` in reverse order.
*
* @name reduceRight
* @static
* @memberOf module:Collections
* @method
* @see [async.reduce]{@link module:Collections.reduce}
* @alias foldr
* @category Collection
* @param {Array} array - A collection to iterate over.
* @param {*} memo - The initial state of the reduction.
* @param {Function} iteratee - A function applied to each item in the
* array to produce the next step in the reduction. The `iteratee` is passed a
* `callback(err, reduction)` which accepts an optional error as its first
* argument, and the state of the reduction as the second. If an error is
* passed to the callback, the reduction is stopped and the main `callback` is
* immediately called with the error. Invoked with (memo, item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Result is the reduced value. Invoked with
* (err, result).
*/
function reduceRight(array, memo, iteratee, callback) {
var reversed = slice.call(array).reverse();
reduce(reversed, memo, iteratee, callback);
}
/**
* Wraps the function in another function that always returns data even when it
* errors.
*
* The object returned has either the property `error` or `value`.
*
* @name reflect
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} fn - The function you want to wrap
* @returns {Function} - A function that always passes null to it's callback as
* the error. The second argument to the callback will be an `object` with
* either an `error` or a `value` property.
* @example
*
* async.parallel([
* async.reflect(function(callback) {
* // do some stuff ...
* callback(null, 'one');
* }),
* async.reflect(function(callback) {
* // do some more stuff but error ...
* callback('bad stuff happened');
* }),
* async.reflect(function(callback) {
* // do some more stuff ...
* callback(null, 'two');
* })
* ],
* // optional callback
* function(err, results) {
* // values
* // results[0].value = 'one'
* // results[1].error = 'bad stuff happened'
* // results[2].value = 'two'
* });
*/
function reflect(fn) {
return initialParams(function reflectOn(args, reflectCallback) {
args.push(rest(function callback(err, cbArgs) {
if (err) {
reflectCallback(null, {
error: err
});
} else {
var value = null;
if (cbArgs.length === 1) {
value = cbArgs[0];
} else if (cbArgs.length > 1) {
value = cbArgs;
}
reflectCallback(null, {
value: value
});
}
}));
return fn.apply(this, args);
});
}
function reject$1(eachfn, arr, iteratee, callback) {
_filter(eachfn, arr, function (value, cb) {
iteratee(value, function (err, v) {
cb(err, !v);
});
}, callback);
}
/**
* The opposite of [`filter`]{@link module:Collections.filter}. Removes values that pass an `async` truth test.
*
* @name reject
* @static
* @memberOf module:Collections
* @method
* @see [async.filter]{@link module:Collections.filter}
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The `iteratee` is passed a `callback(err, truthValue)`, which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Invoked with (err, results).
* @example
*
* async.reject(['file1','file2','file3'], function(filePath, callback) {
* fs.access(filePath, function(err) {
* callback(null, !err)
* });
* }, function(err, results) {
* // results now equals an array of missing files
* createFiles(results);
* });
*/
var reject = doParallel(reject$1);
/**
* A helper function that wraps an array or an object of functions with reflect.
*
* @name reflectAll
* @static
* @memberOf module:Utils
* @method
* @see [async.reflect]{@link module:Utils.reflect}
* @category Util
* @param {Array} tasks - The array of functions to wrap in `async.reflect`.
* @returns {Array} Returns an array of functions, each function wrapped in
* `async.reflect`
* @example
*
* let tasks = [
* function(callback) {
* setTimeout(function() {
* callback(null, 'one');
* }, 200);
* },
* function(callback) {
* // do some more stuff but error ...
* callback(new Error('bad stuff happened'));
* },
* function(callback) {
* setTimeout(function() {
* callback(null, 'two');
* }, 100);
* }
* ];
*
* async.parallel(async.reflectAll(tasks),
* // optional callback
* function(err, results) {
* // values
* // results[0].value = 'one'
* // results[1].error = Error('bad stuff happened')
* // results[2].value = 'two'
* });
*
* // an example using an object instead of an array
* let tasks = {
* one: function(callback) {
* setTimeout(function() {
* callback(null, 'one');
* }, 200);
* },
* two: function(callback) {
* callback('two');
* },
* three: function(callback) {
* setTimeout(function() {
* callback(null, 'three');
* }, 100);
* }
* };
*
* async.parallel(async.reflectAll(tasks),
* // optional callback
* function(err, results) {
* // values
* // results.one.value = 'one'
* // results.two.error = 'two'
* // results.three.value = 'three'
* });
*/
function reflectAll(tasks) {
var results;
if (isArray(tasks)) {
results = arrayMap(tasks, reflect);
} else {
results = {};
baseForOwn(tasks, function (task, key) {
results[key] = reflect.call(this, task);
});
}
return results;
}
/**
* The same as [`reject`]{@link module:Collections.reject} but runs a maximum of `limit` async operations at a
* time.
*
* @name rejectLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.reject]{@link module:Collections.reject}
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The `iteratee` is passed a `callback(err, truthValue)`, which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Invoked with (err, results).
*/
var rejectLimit = doParallelLimit(reject$1);
/**
* The same as [`reject`]{@link module:Collections.reject} but runs only a single async operation at a time.
*
* @name rejectSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.reject]{@link module:Collections.reject}
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in `coll`.
* The `iteratee` is passed a `callback(err, truthValue)`, which must be called
* with a boolean argument once it has completed. Invoked with (item, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Invoked with (err, results).
*/
var rejectSeries = doLimit(rejectLimit, 1);
/**
* Creates a function that returns `value`.
*
* @static
* @memberOf _
* @since 2.4.0
* @category Util
* @param {*} value The value to return from the new function.
* @returns {Function} Returns the new constant function.
* @example
*
* var objects = _.times(2, _.constant({ 'a': 1 }));
*
* console.log(objects);
* // => [{ 'a': 1 }, { 'a': 1 }]
*
* console.log(objects[0] === objects[1]);
* // => true
*/
function constant$1(value) {
return function() {
return value;
};
}
/**
* Attempts to get a successful response from `task` no more than `times` times
* before returning an error. If the task is successful, the `callback` will be
* passed the result of the successful task. If all attempts fail, the callback
* will be passed the error and result (if any) of the final attempt.
*
* @name retry
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Object|number} [opts = {times: 5, interval: 0}| 5] - Can be either an
* object with `times` and `interval` or a number.
* * `times` - The number of attempts to make before giving up. The default
* is `5`.
* * `interval` - The time to wait between retries, in milliseconds. The
* default is `0`. The interval may also be specified as a function of the
* retry count (see example).
* * `errorFilter` - An optional synchronous function that is invoked on
* erroneous result. If it returns `true` the retry attempts will continue;
* if the function returns `false` the retry flow is aborted with the current
* attempt's error and result being returned to the final callback.
* Invoked with (err).
* * If `opts` is a number, the number specifies the number of times to retry,
* with the default interval of `0`.
* @param {Function} task - A function which receives two arguments: (1) a
* `callback(err, result)` which must be called when finished, passing `err`
* (which can be `null`) and the `result` of the function's execution, and (2)
* a `results` object, containing the results of the previously executed
* functions (if nested inside another control flow). Invoked with
* (callback, results).
* @param {Function} [callback] - An optional callback which is called when the
* task has succeeded, or after the final failed attempt. It receives the `err`
* and `result` arguments of the last attempt at completing the `task`. Invoked
* with (err, results).
* @example
*
* // The `retry` function can be used as a stand-alone control flow by passing
* // a callback, as shown below:
*
* // try calling apiMethod 3 times
* async.retry(3, apiMethod, function(err, result) {
* // do something with the result
* });
*
* // try calling apiMethod 3 times, waiting 200 ms between each retry
* async.retry({times: 3, interval: 200}, apiMethod, function(err, result) {
* // do something with the result
* });
*
* // try calling apiMethod 10 times with exponential backoff
* // (i.e. intervals of 100, 200, 400, 800, 1600, ... milliseconds)
* async.retry({
* times: 10,
* interval: function(retryCount) {
* return 50 * Math.pow(2, retryCount);
* }
* }, apiMethod, function(err, result) {
* // do something with the result
* });
*
* // try calling apiMethod the default 5 times no delay between each retry
* async.retry(apiMethod, function(err, result) {
* // do something with the result
* });
*
* // try calling apiMethod only when error condition satisfies, all other
* // errors will abort the retry control flow and return to final callback
* async.retry({
* errorFilter: function(err) {
* return err.message === 'Temporary error'; // only retry on a specific error
* }
* }, apiMethod, function(err, result) {
* // do something with the result
* });
*
* // It can also be embedded within other control flow functions to retry
* // individual methods that are not as reliable, like this:
* async.auto({
* users: api.getUsers.bind(api),
* payments: async.retry(3, api.getPayments.bind(api))
* }, function(err, results) {
* // do something with the results
* });
*
*/
function retry(opts, task, callback) {
var DEFAULT_TIMES = 5;
var DEFAULT_INTERVAL = 0;
var options = {
times: DEFAULT_TIMES,
intervalFunc: constant$1(DEFAULT_INTERVAL)
};
function parseTimes(acc, t) {
if (typeof t === 'object') {
acc.times = +t.times || DEFAULT_TIMES;
acc.intervalFunc = typeof t.interval === 'function' ? t.interval : constant$1(+t.interval || DEFAULT_INTERVAL);
acc.errorFilter = t.errorFilter;
} else if (typeof t === 'number' || typeof t === 'string') {
acc.times = +t || DEFAULT_TIMES;
} else {
throw new Error("Invalid arguments for async.retry");
}
}
if (arguments.length < 3 && typeof opts === 'function') {
callback = task || noop;
task = opts;
} else {
parseTimes(options, opts);
callback = callback || noop;
}
if (typeof task !== 'function') {
throw new Error("Invalid arguments for async.retry");
}
var attempt = 1;
function retryAttempt() {
task(function (err) {
if (err && attempt++ < options.times && (typeof options.errorFilter != 'function' || options.errorFilter(err))) {
setTimeout(retryAttempt, options.intervalFunc(attempt));
} else {
callback.apply(null, arguments);
}
});
}
retryAttempt();
}
/**
* A close relative of [`retry`]{@link module:ControlFlow.retry}. This method wraps a task and makes it
* retryable, rather than immediately calling it with retries.
*
* @name retryable
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.retry]{@link module:ControlFlow.retry}
* @category Control Flow
* @param {Object|number} [opts = {times: 5, interval: 0}| 5] - optional
* options, exactly the same as from `retry`
* @param {Function} task - the asynchronous function to wrap
* @returns {Functions} The wrapped function, which when invoked, will retry on
* an error, based on the parameters specified in `opts`.
* @example
*
* async.auto({
* dep1: async.retryable(3, getFromFlakyService),
* process: ["dep1", async.retryable(3, function (results, cb) {
* maybeProcessData(results.dep1, cb);
* })]
* }, callback);
*/
var retryable = function (opts, task) {
if (!task) {
task = opts;
opts = null;
}
return initialParams(function (args, callback) {
function taskFn(cb) {
task.apply(null, args.concat(cb));
}
if (opts) retry(opts, taskFn, callback);else retry(taskFn, callback);
});
};
/**
* Run the functions in the `tasks` collection in series, each one running once
* the previous function has completed. If any functions in the series pass an
* error to its callback, no more functions are run, and `callback` is
* immediately called with the value of the error. Otherwise, `callback`
* receives an array of results when `tasks` have completed.
*
* It is also possible to use an object instead of an array. Each property will
* be run as a function, and the results will be passed to the final `callback`
* as an object instead of an array. This can be a more readable way of handling
* results from {@link async.series}.
*
* **Note** that while many implementations preserve the order of object
* properties, the [ECMAScript Language Specification](http://www.ecma-international.org/ecma-262/5.1/#sec-8.6)
* explicitly states that
*
* > The mechanics and order of enumerating the properties is not specified.
*
* So if you rely on the order in which your series of functions are executed,
* and want this to work on all platforms, consider using an array.
*
* @name series
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Array|Iterable|Object} tasks - A collection containing functions to run, each
* function is passed a `callback(err, result)` it must call on completion with
* an error `err` (which can be `null`) and an optional `result` value.
* @param {Function} [callback] - An optional callback to run once all the
* functions have completed. This function gets a results array (or object)
* containing all the result arguments passed to the `task` callbacks. Invoked
* with (err, result).
* @example
* async.series([
* function(callback) {
* // do some stuff ...
* callback(null, 'one');
* },
* function(callback) {
* // do some more stuff ...
* callback(null, 'two');
* }
* ],
* // optional callback
* function(err, results) {
* // results is now equal to ['one', 'two']
* });
*
* async.series({
* one: function(callback) {
* setTimeout(function() {
* callback(null, 1);
* }, 200);
* },
* two: function(callback){
* setTimeout(function() {
* callback(null, 2);
* }, 100);
* }
* }, function(err, results) {
* // results is now equal to: {one: 1, two: 2}
* });
*/
function series(tasks, callback) {
_parallel(eachOfSeries, tasks, callback);
}
/**
* Returns `true` if at least one element in the `coll` satisfies an async test.
* If any iteratee call returns `true`, the main `callback` is immediately
* called.
*
* @name some
* @static
* @memberOf module:Collections
* @method
* @alias any
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in the array
* in parallel. The iteratee is passed a `callback(err, truthValue)` which must
* be called with a boolean argument once it has completed. Invoked with
* (item, callback).
* @param {Function} [callback] - A callback which is called as soon as any
* iteratee returns `true`, or after all the iteratee functions have finished.
* Result will be either `true` or `false` depending on the values of the async
* tests. Invoked with (err, result).
* @example
*
* async.some(['file1','file2','file3'], function(filePath, callback) {
* fs.access(filePath, function(err) {
* callback(null, !err)
* });
* }, function(err, result) {
* // if result is true then at least one of the files exists
* });
*/
var some = doParallel(_createTester(Boolean, identity));
/**
* The same as [`some`]{@link module:Collections.some} but runs a maximum of `limit` async operations at a time.
*
* @name someLimit
* @static
* @memberOf module:Collections
* @method
* @see [async.some]{@link module:Collections.some}
* @alias anyLimit
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - A truth test to apply to each item in the array
* in parallel. The iteratee is passed a `callback(err, truthValue)` which must
* be called with a boolean argument once it has completed. Invoked with
* (item, callback).
* @param {Function} [callback] - A callback which is called as soon as any
* iteratee returns `true`, or after all the iteratee functions have finished.
* Result will be either `true` or `false` depending on the values of the async
* tests. Invoked with (err, result).
*/
var someLimit = doParallelLimit(_createTester(Boolean, identity));
/**
* The same as [`some`]{@link module:Collections.some} but runs only a single async operation at a time.
*
* @name someSeries
* @static
* @memberOf module:Collections
* @method
* @see [async.some]{@link module:Collections.some}
* @alias anySeries
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A truth test to apply to each item in the array
* in parallel. The iteratee is passed a `callback(err, truthValue)` which must
* be called with a boolean argument once it has completed. Invoked with
* (item, callback).
* @param {Function} [callback] - A callback which is called as soon as any
* iteratee returns `true`, or after all the iteratee functions have finished.
* Result will be either `true` or `false` depending on the values of the async
* tests. Invoked with (err, result).
*/
var someSeries = doLimit(someLimit, 1);
/**
* Sorts a list by the results of running each `coll` value through an async
* `iteratee`.
*
* @name sortBy
* @static
* @memberOf module:Collections
* @method
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {Function} iteratee - A function to apply to each item in `coll`.
* The iteratee is passed a `callback(err, sortValue)` which must be called once
* it has completed with an error (which can be `null`) and a value to use as
* the sort criteria. Invoked with (item, callback).
* @param {Function} callback - A callback which is called after all the
* `iteratee` functions have finished, or an error occurs. Results is the items
* from the original `coll` sorted by the values returned by the `iteratee`
* calls. Invoked with (err, results).
* @example
*
* async.sortBy(['file1','file2','file3'], function(file, callback) {
* fs.stat(file, function(err, stats) {
* callback(err, stats.mtime);
* });
* }, function(err, results) {
* // results is now the original array of files sorted by
* // modified date
* });
*
* // By modifying the callback parameter the
* // sorting order can be influenced:
*
* // ascending order
* async.sortBy([1,9,3,5], function(x, callback) {
* callback(null, x);
* }, function(err,result) {
* // result callback
* });
*
* // descending order
* async.sortBy([1,9,3,5], function(x, callback) {
* callback(null, x*-1); //<- x*-1 instead of x, turns the order around
* }, function(err,result) {
* // result callback
* });
*/
function sortBy(coll, iteratee, callback) {
map(coll, function (x, callback) {
iteratee(x, function (err, criteria) {
if (err) return callback(err);
callback(null, { value: x, criteria: criteria });
});
}, function (err, results) {
if (err) return callback(err);
callback(null, arrayMap(results.sort(comparator), baseProperty('value')));
});
function comparator(left, right) {
var a = left.criteria,
b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}
}
/**
* Sets a time limit on an asynchronous function. If the function does not call
* its callback within the specified milliseconds, it will be called with a
* timeout error. The code property for the error object will be `'ETIMEDOUT'`.
*
* @name timeout
* @static
* @memberOf module:Utils
* @method
* @category Util
* @param {Function} asyncFn - The asynchronous function you want to set the
* time limit.
* @param {number} milliseconds - The specified time limit.
* @param {*} [info] - Any variable you want attached (`string`, `object`, etc)
* to timeout Error for more information..
* @returns {Function} Returns a wrapped function that can be used with any of
* the control flow functions. Invoke this function with the same
* parameters as you would `asyncFunc`.
* @example
*
* function myFunction(foo, callback) {
* doAsyncTask(foo, function(err, data) {
* // handle errors
* if (err) return callback(err);
*
* // do some stuff ...
*
* // return processed data
* return callback(null, data);
* });
* }
*
* var wrapped = async.timeout(myFunction, 1000);
*
* // call `wrapped` as you would `myFunction`
* wrapped({ bar: 'bar' }, function(err, data) {
* // if `myFunction` takes < 1000 ms to execute, `err`
* // and `data` will have their expected values
*
* // else `err` will be an Error with the code 'ETIMEDOUT'
* });
*/
function timeout(asyncFn, milliseconds, info) {
var originalCallback, timer;
var timedOut = false;
function injectedCallback() {
if (!timedOut) {
originalCallback.apply(null, arguments);
clearTimeout(timer);
}
}
function timeoutCallback() {
var name = asyncFn.name || 'anonymous';
var error = new Error('Callback function "' + name + '" timed out.');
error.code = 'ETIMEDOUT';
if (info) {
error.info = info;
}
timedOut = true;
originalCallback(error);
}
return initialParams(function (args, origCallback) {
originalCallback = origCallback;
// setup timer and call original function
timer = setTimeout(timeoutCallback, milliseconds);
asyncFn.apply(null, args.concat(injectedCallback));
});
}
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeCeil = Math.ceil;
var nativeMax$1 = Math.max;
/**
* The base implementation of `_.range` and `_.rangeRight` which doesn't
* coerce arguments.
*
* @private
* @param {number} start The start of the range.
* @param {number} end The end of the range.
* @param {number} step The value to increment or decrement by.
* @param {boolean} [fromRight] Specify iterating from right to left.
* @returns {Array} Returns the range of numbers.
*/
function baseRange(start, end, step, fromRight) {
var index = -1,
length = nativeMax$1(nativeCeil((end - start) / (step || 1)), 0),
result = Array(length);
while (length--) {
result[fromRight ? length : ++index] = start;
start += step;
}
return result;
}
/**
* The same as [times]{@link module:ControlFlow.times} but runs a maximum of `limit` async operations at a
* time.
*
* @name timesLimit
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.times]{@link module:ControlFlow.times}
* @category Control Flow
* @param {number} count - The number of times to run the function.
* @param {number} limit - The maximum number of async operations at a time.
* @param {Function} iteratee - The function to call `n` times. Invoked with the
* iteration index and a callback (n, next).
* @param {Function} callback - see [async.map]{@link module:Collections.map}.
*/
function timeLimit(count, limit, iteratee, callback) {
mapLimit(baseRange(0, count, 1), limit, iteratee, callback);
}
/**
* Calls the `iteratee` function `n` times, and accumulates results in the same
* manner you would use with [map]{@link module:Collections.map}.
*
* @name times
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.map]{@link module:Collections.map}
* @category Control Flow
* @param {number} n - The number of times to run the function.
* @param {Function} iteratee - The function to call `n` times. Invoked with the
* iteration index and a callback (n, next).
* @param {Function} callback - see {@link module:Collections.map}.
* @example
*
* // Pretend this is some complicated async factory
* var createUser = function(id, callback) {
* callback(null, {
* id: 'user' + id
* });
* };
*
* // generate 5 users
* async.times(5, function(n, next) {
* createUser(n, function(err, user) {
* next(err, user);
* });
* }, function(err, users) {
* // we should now have 5 users
* });
*/
var times = doLimit(timeLimit, Infinity);
/**
* The same as [times]{@link module:ControlFlow.times} but runs only a single async operation at a time.
*
* @name timesSeries
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.times]{@link module:ControlFlow.times}
* @category Control Flow
* @param {number} n - The number of times to run the function.
* @param {Function} iteratee - The function to call `n` times. Invoked with the
* iteration index and a callback (n, next).
* @param {Function} callback - see {@link module:Collections.map}.
*/
var timesSeries = doLimit(timeLimit, 1);
/**
* A relative of `reduce`. Takes an Object or Array, and iterates over each
* element in series, each step potentially mutating an `accumulator` value.
* The type of the accumulator defaults to the type of collection passed in.
*
* @name transform
* @static
* @memberOf module:Collections
* @method
* @category Collection
* @param {Array|Iterable|Object} coll - A collection to iterate over.
* @param {*} [accumulator] - The initial state of the transform. If omitted,
* it will default to an empty Object or Array, depending on the type of `coll`
* @param {Function} iteratee - A function applied to each item in the
* collection that potentially modifies the accumulator. The `iteratee` is
* passed a `callback(err)` which accepts an optional error as its first
* argument. If an error is passed to the callback, the transform is stopped
* and the main `callback` is immediately called with the error.
* Invoked with (accumulator, item, key, callback).
* @param {Function} [callback] - A callback which is called after all the
* `iteratee` functions have finished. Result is the transformed accumulator.
* Invoked with (err, result).
* @example
*
* async.transform([1,2,3], function(acc, item, index, callback) {
* // pointless async:
* process.nextTick(function() {
* acc.push(item * 2)
* callback(null)
* });
* }, function(err, result) {
* // result is now equal to [2, 4, 6]
* });
*
* @example
*
* async.transform({a: 1, b: 2, c: 3}, function (obj, val, key, callback) {
* setImmediate(function () {
* obj[key] = val * 2;
* callback();
* })
* }, function (err, result) {
* // result is equal to {a: 2, b: 4, c: 6}
* })
*/
function transform(coll, accumulator, iteratee, callback) {
if (arguments.length === 3) {
callback = iteratee;
iteratee = accumulator;
accumulator = isArray(coll) ? [] : {};
}
callback = once(callback || noop);
eachOf(coll, function (v, k, cb) {
iteratee(accumulator, v, k, cb);
}, function (err) {
callback(err, accumulator);
});
}
/**
* Undoes a [memoize]{@link module:Utils.memoize}d function, reverting it to the original,
* unmemoized form. Handy for testing.
*
* @name unmemoize
* @static
* @memberOf module:Utils
* @method
* @see [async.memoize]{@link module:Utils.memoize}
* @category Util
* @param {Function} fn - the memoized function
* @returns {Function} a function that calls the original unmemoized function
*/
function unmemoize(fn) {
return function () {
return (fn.unmemoized || fn).apply(null, arguments);
};
}
/**
* Repeatedly call `iteratee`, while `test` returns `true`. Calls `callback` when
* stopped, or an error occurs.
*
* @name whilst
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Function} test - synchronous truth test to perform before each
* execution of `iteratee`. Invoked with ().
* @param {Function} iteratee - A function which is called each time `test` passes.
* The function is passed a `callback(err)`, which must be called once it has
* completed with an optional `err` argument. Invoked with (callback).
* @param {Function} [callback] - A callback which is called after the test
* function has failed and repeated execution of `iteratee` has stopped. `callback`
* will be passed an error and any arguments passed to the final `iteratee`'s
* callback. Invoked with (err, [results]);
* @returns undefined
* @example
*
* var count = 0;
* async.whilst(
* function() { return count < 5; },
* function(callback) {
* count++;
* setTimeout(function() {
* callback(null, count);
* }, 1000);
* },
* function (err, n) {
* // 5 seconds have passed, n = 5
* }
* );
*/
function whilst(test, iteratee, callback) {
callback = onlyOnce(callback || noop);
if (!test()) return callback(null);
var next = rest(function (err, args) {
if (err) return callback(err);
if (test()) return iteratee(next);
callback.apply(null, [null].concat(args));
});
iteratee(next);
}
/**
* Repeatedly call `fn` until `test` returns `true`. Calls `callback` when
* stopped, or an error occurs. `callback` will be passed an error and any
* arguments passed to the final `fn`'s callback.
*
* The inverse of [whilst]{@link module:ControlFlow.whilst}.
*
* @name until
* @static
* @memberOf module:ControlFlow
* @method
* @see [async.whilst]{@link module:ControlFlow.whilst}
* @category Control Flow
* @param {Function} test - synchronous truth test to perform before each
* execution of `fn`. Invoked with ().
* @param {Function} fn - A function which is called each time `test` fails.
* The function is passed a `callback(err)`, which must be called once it has
* completed with an optional `err` argument. Invoked with (callback).
* @param {Function} [callback] - A callback which is called after the test
* function has passed and repeated execution of `fn` has stopped. `callback`
* will be passed an error and any arguments passed to the final `fn`'s
* callback. Invoked with (err, [results]);
*/
function until(test, fn, callback) {
whilst(function () {
return !test.apply(this, arguments);
}, fn, callback);
}
/**
* Runs the `tasks` array of functions in series, each passing their results to
* the next in the array. However, if any of the `tasks` pass an error to their
* own callback, the next function is not executed, and the main `callback` is
* immediately called with the error.
*
* @name waterfall
* @static
* @memberOf module:ControlFlow
* @method
* @category Control Flow
* @param {Array} tasks - An array of functions to run, each function is passed
* a `callback(err, result1, result2, ...)` it must call on completion. The
* first argument is an error (which can be `null`) and any further arguments
* will be passed as arguments in order to the next task.
* @param {Function} [callback] - An optional callback to run once all the
* functions have completed. This will be passed the results of the last task's
* callback. Invoked with (err, [results]).
* @returns undefined
* @example
*
* async.waterfall([
* function(callback) {
* callback(null, 'one', 'two');
* },
* function(arg1, arg2, callback) {
* // arg1 now equals 'one' and arg2 now equals 'two'
* callback(null, 'three');
* },
* function(arg1, callback) {
* // arg1 now equals 'three'
* callback(null, 'done');
* }
* ], function (err, result) {
* // result now equals 'done'
* });
*
* // Or, with named functions:
* async.waterfall([
* myFirstFunction,
* mySecondFunction,
* myLastFunction,
* ], function (err, result) {
* // result now equals 'done'
* });
* function myFirstFunction(callback) {
* callback(null, 'one', 'two');
* }
* function mySecondFunction(arg1, arg2, callback) {
* // arg1 now equals 'one' and arg2 now equals 'two'
* callback(null, 'three');
* }
* function myLastFunction(arg1, callback) {
* // arg1 now equals 'three'
* callback(null, 'done');
* }
*/
var waterfall = function (tasks, callback) {
callback = once(callback || noop);
if (!isArray(tasks)) return callback(new Error('First argument to waterfall must be an array of functions'));
if (!tasks.length) return callback();
var taskIndex = 0;
function nextTask(args) {
if (taskIndex === tasks.length) {
return callback.apply(null, [null].concat(args));
}
var taskCallback = onlyOnce(rest(function (err, args) {
if (err) {
return callback.apply(null, [err].concat(args));
}
nextTask(args);
}));
args.push(taskCallback);
var task = tasks[taskIndex++];
task.apply(null, args);
}
nextTask([]);
};
/**
* Async is a utility module which provides straight-forward, powerful functions
* for working with asynchronous JavaScript. Although originally designed for
* use with [Node.js](http://nodejs.org) and installable via
* `npm install --save async`, it can also be used directly in the browser.
* @module async
*/
/**
* A collection of `async` functions for manipulating collections, such as
* arrays and objects.
* @module Collections
*/
/**
* A collection of `async` functions for controlling the flow through a script.
* @module ControlFlow
*/
/**
* A collection of `async` utility functions.
* @module Utils
*/
var index = {
applyEach: applyEach,
applyEachSeries: applyEachSeries,
apply: apply$2,
asyncify: asyncify,
auto: auto,
autoInject: autoInject,
cargo: cargo,
compose: compose,
concat: concat,
concatSeries: concatSeries,
constant: constant,
detect: detect,
detectLimit: detectLimit,
detectSeries: detectSeries,
dir: dir,
doDuring: doDuring,
doUntil: doUntil,
doWhilst: doWhilst,
during: during,
each: eachLimit,
eachLimit: eachLimit$1,
eachOf: eachOf,
eachOfLimit: eachOfLimit,
eachOfSeries: eachOfSeries,
eachSeries: eachSeries,
ensureAsync: ensureAsync,
every: every,
everyLimit: everyLimit,
everySeries: everySeries,
filter: filter,
filterLimit: filterLimit,
filterSeries: filterSeries,
forever: forever,
log: log,
map: map,
mapLimit: mapLimit,
mapSeries: mapSeries,
mapValues: mapValues,
mapValuesLimit: mapValuesLimit,
mapValuesSeries: mapValuesSeries,
memoize: memoize,
nextTick: nextTick,
parallel: parallelLimit,
parallelLimit: parallelLimit$1,
priorityQueue: priorityQueue,
queue: queue$1,
race: race,
reduce: reduce,
reduceRight: reduceRight,
reflect: reflect,
reflectAll: reflectAll,
reject: reject,
rejectLimit: rejectLimit,
rejectSeries: rejectSeries,
retry: retry,
retryable: retryable,
seq: seq$1,
series: series,
setImmediate: setImmediate$1,
some: some,
someLimit: someLimit,
someSeries: someSeries,
sortBy: sortBy,
timeout: timeout,
times: times,
timesLimit: timeLimit,
timesSeries: timesSeries,
transform: transform,
unmemoize: unmemoize,
until: until,
waterfall: waterfall,
whilst: whilst,
// aliases
all: every,
any: some,
forEach: eachLimit,
forEachSeries: eachSeries,
forEachLimit: eachLimit$1,
forEachOf: eachOf,
forEachOfSeries: eachOfSeries,
forEachOfLimit: eachOfLimit,
inject: reduce,
foldl: reduce,
foldr: reduceRight,
select: filter,
selectLimit: filterLimit,
selectSeries: filterSeries,
wrapSync: asyncify
};
exports['default'] = index;
exports.applyEach = applyEach;
exports.applyEachSeries = applyEachSeries;
exports.apply = apply$2;
exports.asyncify = asyncify;
exports.auto = auto;
exports.autoInject = autoInject;
exports.cargo = cargo;
exports.compose = compose;
exports.concat = concat;
exports.concatSeries = concatSeries;
exports.constant = constant;
exports.detect = detect;
exports.detectLimit = detectLimit;
exports.detectSeries = detectSeries;
exports.dir = dir;
exports.doDuring = doDuring;
exports.doUntil = doUntil;
exports.doWhilst = doWhilst;
exports.during = during;
exports.each = eachLimit;
exports.eachLimit = eachLimit$1;
exports.eachOf = eachOf;
exports.eachOfLimit = eachOfLimit;
exports.eachOfSeries = eachOfSeries;
exports.eachSeries = eachSeries;
exports.ensureAsync = ensureAsync;
exports.every = every;
exports.everyLimit = everyLimit;
exports.everySeries = everySeries;
exports.filter = filter;
exports.filterLimit = filterLimit;
exports.filterSeries = filterSeries;
exports.forever = forever;
exports.log = log;
exports.map = map;
exports.mapLimit = mapLimit;
exports.mapSeries = mapSeries;
exports.mapValues = mapValues;
exports.mapValuesLimit = mapValuesLimit;
exports.mapValuesSeries = mapValuesSeries;
exports.memoize = memoize;
exports.nextTick = nextTick;
exports.parallel = parallelLimit;
exports.parallelLimit = parallelLimit$1;
exports.priorityQueue = priorityQueue;
exports.queue = queue$1;
exports.race = race;
exports.reduce = reduce;
exports.reduceRight = reduceRight;
exports.reflect = reflect;
exports.reflectAll = reflectAll;
exports.reject = reject;
exports.rejectLimit = rejectLimit;
exports.rejectSeries = rejectSeries;
exports.retry = retry;
exports.retryable = retryable;
exports.seq = seq$1;
exports.series = series;
exports.setImmediate = setImmediate$1;
exports.some = some;
exports.someLimit = someLimit;
exports.someSeries = someSeries;
exports.sortBy = sortBy;
exports.timeout = timeout;
exports.times = times;
exports.timesLimit = timeLimit;
exports.timesSeries = timesSeries;
exports.transform = transform;
exports.unmemoize = unmemoize;
exports.until = until;
exports.waterfall = waterfall;
exports.whilst = whilst;
exports.all = every;
exports.allLimit = everyLimit;
exports.allSeries = everySeries;
exports.any = some;
exports.anyLimit = someLimit;
exports.anySeries = someSeries;
exports.find = detect;
exports.findLimit = detectLimit;
exports.findSeries = detectSeries;
exports.forEach = eachLimit;
exports.forEachSeries = eachSeries;
exports.forEachLimit = eachLimit$1;
exports.forEachOf = eachOf;
exports.forEachOfSeries = eachOfSeries;
exports.forEachOfLimit = eachOfLimit;
exports.inject = reduce;
exports.foldl = reduce;
exports.foldr = reduceRight;
exports.select = filter;
exports.selectLimit = filterLimit;
exports.selectSeries = filterSeries;
exports.wrapSync = asyncify;
Object.defineProperty(exports, '__esModule', { value: true });
})));
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| index.js | 100% | (5 / 5) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (5 / 5) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | 1 1 1 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
var SG = require('strong-globalize');
SG.SetRootDir(__dirname);
exports.PhaseList = require('./lib/phase-list');
exports.Phase = require('./lib/phase');
exports.mergePhaseNameLists = require('./lib/merge-name-lists');
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| merge-name-lists.js | 10% | (2 / 20) | 0% | (0 / 8) | 0% | (0 / 1) | 10.53% | (2 / 19) | |
| phase-list.js | 21.65% | (21 / 97) | 0% | (0 / 38) | 0% | (0 / 19) | 22.83% | (21 / 92) | |
| phase.js | 31.43% | (11 / 35) | 0% | (0 / 10) | 0% | (0 / 8) | 31.43% | (11 / 35) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
/**
* Extend the list of builtin phases by merging in an array of phases
* requested by a user while preserving the relative order of phases
* as specified by both arrays.
*
* If the first new name does not match any existing phase, it is inserted
* as the first phase in the new list. The same applies for the second phase,
* and so on, until an existing phase is found.
*
* Any new names in the middle of the array are inserted immediatelly after
* the last common phase. For example, extending
* `["initial", "session", "auth"]` with `["initial", "preauth", "auth"]`
* results in `["initial", "preauth", "session", "auth"]`.
*
*
* **Example**
*
* ```js
* var result = mergePhaseNameLists(
* ['initial', 'session', 'auth', 'routes', 'files', 'final'],
* ['initial', 'postinit', 'preauth', 'auth',
* 'routes', 'subapps', 'final', 'last']
* );
*
* // result: [
* // 'initial', 'postinit', 'preauth', 'session', 'auth',
* // 'routes', 'subapps', 'files', 'final', 'last'
* // ]
* ```
*
* @param {Array} currentNames The current list of phase names.
* @param {Array} namesToMerge The items to add (zip merge) into the target
* array.
* @returns {Array} A new array containing combined items from both arrays.
*
* @header mergePhaseNameLists
*/
module.exports = function mergePhaseNameLists(currentNames, namesToMerge) {
if (!namesToMerge.length) return currentNames.slice();
var targetArray = currentNames.slice();
var targetIx = targetArray.indexOf(namesToMerge[0]);
if (targetIx === -1) {
// the first new item does not match any existing one
// start adding the new items at the start of the list
targetArray.splice(0, 0, namesToMerge[0]);
targetIx = 0;
}
// merge (zip) two arrays
for (var sourceIx = 1; sourceIx < namesToMerge.length; sourceIx++) {
var valueToAdd = namesToMerge[sourceIx];
var previousValue = namesToMerge[sourceIx - 1];
var existingIx = targetArray.indexOf(valueToAdd, targetIx);
if (existingIx === -1) {
// A new phase - try to add it after the last one,
// unless it was already registered
if (targetArray.indexOf(valueToAdd) !== -1) {
throw new Error(g.f('Ordering conflict: cannot add "%s' +
'" after "%s", because the opposite order was ' +
' already specified', valueToAdd, previousValue));
}
var previousIx = targetArray.indexOf(previousValue);
targetArray.splice(previousIx + 1, 0, valueToAdd);
} else {
// An existing phase - move the pointer
targetIx = existingIx;
}
}
return targetArray;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var Phase = require('./phase');
var zipMerge = require('./merge-name-lists');
var async = require('async');
module.exports = PhaseList;
/**
* An ordered list of phases.
*
* ```js
* var PhaseList = require('loopback-phase').PhaseList;
* var phases = new PhaseList();
* phases.add('my-phase');
* ```
*
* @class PhaseList
*/
function PhaseList() {
this._phases = [];
this._phaseMap = {};
}
/**
* Get the first `Phase` in the list.
*
* @returns {Phase} The first phase.
*/
PhaseList.prototype.first = function() {
return this._phases[0];
};
/**
* Get the last `Phase` in the list.
*
* @returns {Phase} The last phase.
*/
PhaseList.prototype.last = function() {
return this._phases[this._phases.length - 1];
};
/**
* Add one or more phases to the list.
*
* @param {Phase|String|String[]} phase The phase (or phases) to be added.
* @returns {Phase|Phase[]} The added phase or phases.
*/
PhaseList.prototype.add = function(phase) {
var phaseList = this;
var phaseArray = Array.isArray(phase) ? phase : null;
if(phaseArray) {
return phaseArray.map(phaseList.add.bind(phaseList));
}
phase = this._resolveNameAndAddToMap(phase);
this._phases.push(phase);
return phase;
};
PhaseList.prototype._resolveNameAndAddToMap = function(phaseOrName) {
var phase = phaseOrName;
if(typeof phase === 'string') {
phase = new Phase(phase);
}
if (phase.id in this._phaseMap) {
throw new Error(g.f('Phase "%s" already exists.', phase.id));
}
if(!phase.__isPhase__) {
throw new Error(g.f('Cannot add a non phase object to a {{PhaseList}}'));
}
this._phaseMap[phase.id] = phase;
return phase;
};
/**
* Add a new phase at the specified index.
* @param {Number} index The zero-based index.
* @param {String|String[]} phase The name of the phase to add.
* @returns {Phase} The added phase.
*/
PhaseList.prototype.addAt = function(index, phase) {
phase = this._resolveNameAndAddToMap(phase);
this._phases.splice(index, 0, phase);
return phase;
};
/**
* Add a new phase as the next one after the given phase.
* @param {String} after The referential phase.
* @param {String|String[]} phase The name of the phase to add.
* @returns {Phase} The added phase.
*/
PhaseList.prototype.addAfter = function(after, phase) {
var ix = this.getPhaseNames().indexOf(after);
if (ix === -1) {
throw new Error(g.f('Unknown phase: %s', after));
}
return this.addAt(ix+1, phase);
};
/**
* Add a new phase as the previous one before the given phase.
* @param {String} before The referential phase.
* @param {String|String[]} phase The name of the phase to add.
* @returns {Phase} The added phase.
*/
PhaseList.prototype.addBefore = function(before, phase) {
var ix = this.getPhaseNames().indexOf(before);
if (ix === -1) {
throw new Error(g.f('Unknown phase: %s', before));
}
return this.addAt(ix, phase);
};
/**
* Remove a `Phase` from the list.
*
* @param {Phase|String} phase The phase to be removed.
* @returns {Phase} The removed phase.
*/
PhaseList.prototype.remove = function(phase) {
var phases = this._phases;
var phaseMap = this._phaseMap;
var phaseId;
if(!phase) return null;
if(typeof phase === 'object') {
phaseId = phase.id;
} else {
phaseId = phase;
phase = phaseMap[phaseId];
}
if(!phase || !phase.__isPhase__) return null;
phases.splice(phases.indexOf(phase), 1);
delete this._phaseMap[phaseId];
return phase;
};
/**
* Merge the provided list of names with the existing phases
* in such way that the order of phases is preserved.
*
* **Example**
*
* ```js
* // Initial list of phases
* phaseList.add(['initial', 'session', 'auth', 'routes', 'files', 'final']);
*
* // zip-merge more phases
* phaseList.zipMerge([
* 'initial', 'postinit', 'preauth', 'auth',
* 'routes', 'subapps', 'final', 'last'
* ]);
*
* // print the result
* console.log('Result:', phaseList.getPhaseNames());
* // Result: [
* // 'initial', 'postinit', 'preauth', 'session', 'auth',
* // 'routes', 'subapps', 'files', 'final', 'last'
* // ]
* ```
*
* @param {String[]} names List of phase names to zip-merge
*/
PhaseList.prototype.zipMerge = function(names) {
if (!names.length) return;
var mergedNames = zipMerge(this.getPhaseNames(), names);
this._phases = mergedNames.map(function(name) {
var existing = this.find(name);
return existing ?
existing :
this._resolveNameAndAddToMap(name);
}, this);
};
/**
* Find a `Phase` from the list.
*
* @param {String} id The phase identifier
* @returns {Phase} The `Phase` with the given `id`.
*/
PhaseList.prototype.find = function(id) {
return this._phaseMap[id] || null;
};
/**
* Find or add a `Phase` from/into the list.
*
* @param {String} id The phase identifier
* @returns {Phase} The `Phase` with the given `id`.
*/
PhaseList.prototype.findOrAdd = function(id) {
var phase = this.find(id);
if(phase) return phase;
return this.add(id);
};
/**
* Get the list of phases as an array of `Phase` objects.
*
* @returns {Phase[]} An array of phases.
*/
PhaseList.prototype.toArray = function() {
return this._phases.slice(0);
};
/**
* Launch the phases contained in the list. If there are no phases
* in the list `process.nextTick` is called with the provided callback.
*
* @param {Object} [context] The context of each `Phase` handler.
* @callback {Function} cb
* @param {Error} err Any error that occured during a phase contained
* in the list.
*/
PhaseList.prototype.run = function(ctx, cb) {
var phases = this._phases;
if(typeof ctx === 'function') {
cb = ctx;
ctx = undefined;
}
if(phases.length) {
async.eachSeries(phases, function(phase, next) {
phase.run(ctx, next);
}, cb);
} else {
process.nextTick(cb);
}
};
/**
* Get an array of phase identifiers.
* @returns {String[]} phaseNames
*/
PhaseList.prototype.getPhaseNames = function() {
return this._phases.map(function(phase) {
return phase.id;
});
};
/**
* Register a phase handler for the given phase (and sub-phase).
*
* **Example**
*
* ```js
* // register via phase.use()
* phaseList.registerHandler('routes', function(ctx, next) { next(); });
* // register via phase.before()
* phaseList.registerHandler('auth:before', function(ctx, next) { next(); });
* // register via phase.after()
* phaseList.registerHandler('auth:after', function(ctx, next) { next(); });
* ```
*
* @param {String} phaseName Name of an existing phase, optionally with
* ":before" or ":after" suffix.
* @param {Function(Object, Function)} handler The handler function to register
* with the given phase.
*/
PhaseList.prototype.registerHandler = function(phaseName, handler) {
var subphase = 'use';
var m = phaseName.match(/^(.+):(before|after)$/);
if (m) {
phaseName = m[1];
subphase = m[2];
}
var phase = this.find(phaseName);
if (!phase) throw new Error(g.f('Unknown phase %s', phaseName));
phase[subphase](handler);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
var async = require('async');
module.exports = Phase;
/**
* A slice of time in an application. Provides hooks to allow
* functions to be executed before, during and after, the defined slice.
* Handlers can be registered to a phase using `before()`, `use()`, or `after()`
* so that they are placed into one of the three stages.
*
* ```js
* var Phase = require('loopback-phase').Phase;
*
* // Create a phase without id
* var anonymousPhase = new Phase();
*
* // Create a named phase
* var myPhase1 = new Phase('my-phase');
*
* // Create a named phase with id & options
* var myPhase2 = new Phase('my-phase', {parallel: true});
*
* // Create a named phase with options only
* var myPhase3 = new Phase({id: 'my-phase', parallel: true});
*
* ```
*
* @class Phase
*
* @prop {String} id The name or identifier of the `Phase`.
* @prop {Object} options The options to configure the `Phase`
*
* @param {String} [id] The name or identifier of the `Phase`.
* @options {Object} [options] Options for the `Phase`
* @property {String} [id] The name or identifier of the Phase
* @property {Boolean} [parallel] To execute handlers in the same stage
* in parallel
* @end
*/
function Phase(id, options) {
if (typeof id === 'object' && options === undefined) {
options = id;
id = options.id;
}
this.id = id;
this.options = options || {};
this.handlers = [];
this.beforeHandlers = [];
this.afterHandlers = [];
}
/**
* Register a phase handler. The handler will be executed
* once the phase is launched. Handlers must callback once
* complete. If the handler calls back with an error, the phase will immediately
* halt execution and call the callback provided to
* `phase.run(callback)`.
*
* **Example**
*
* ```js
* phase.use(function(ctx, next) {
* // specify an error if one occurred...
* var err = null;
* console.log(ctx.message, 'world!'); // => hello world
* next(err);
* });
*
* phase.run({message: 'hello'}, function(err) {
* if(err) return console.error('phase has errored', err);
* console.log('phase has finished');
* });
* ```
*/
Phase.prototype.use = function(handler) {
this.handlers.push(handler);
return this;
};
/**
* Register a phase handler to be executed before the phase begins.
* See `use()` for an example.
*
* @param {Function} handler
*/
Phase.prototype.before = function(handler) {
this.beforeHandlers.push(handler);
return this;
};
/**
* Register a phase handler to be executed after the phase completes.
* See `use()` for an example.
*
* @param {Function} handler
*/
Phase.prototype.after = function(handler) {
this.afterHandlers.push(handler);
return this;
};
/**
* Begin the execution of a phase and its handlers. Provide
* a context object to be passed as the first argument for each handler
* function.
*
* The handlers are executed in serial stage by stage: beforeHandlers, handlers,
* and afterHandlers. Handlers within the same stage are executed in serial by
* default and in parallel only if the options.parallel is true,
*
* @param {Object} [context] The scope applied to each handler function.
* @callback {Function} callback
* @param {Error} err Any `Error` that occurs during the execution of
* the phase.
*/
Phase.prototype.run = function(ctx, cb) {
if (typeof ctx === 'function') {
cb = ctx;
ctx = {};
}
var self = this;
// Run a single handler with ctx
function runHandler(handler, done) {
handler(ctx, done);
}
// Run an array of handlers with ctx
function runHandlers(handlers, done) {
// Only run the handlers in parallel if the options.parallel is true
if (self.options.parallel) {
async.each(handlers, runHandler, done);
} else {
async.eachSeries(handlers, runHandler, done);
}
}
async.eachSeries([this.beforeHandlers, this.handlers, this.afterHandlers],
runHandlers, cb);
};
/**
* Return the `Phase` as a string.
*/
Phase.prototype.toString = function() {
return this.id;
};
// Internal flag to be used instead of
// `instanceof Phase` which breaks
// when there are two instances of
// `require('loopback-phase')
Phase.prototype.__isPhase__ = true;
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| async.js | 15.59% | (97 / 622) | 3.42% | (9 / 263) | 1.97% | (4 / 203) | 15.75% | (97 / 616) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 6 1 1 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | /*!
* async
* https://github.com/caolan/async
*
* Copyright 2010-2014 Caolan McMahon
* Released under the MIT license
*/
/*jshint onevar: false, indent:4 */
/*global setImmediate: false, setTimeout: false, console: false */
(function () {
var async = {};
// global on the server, window in the browser
var root, previous_async;
root = this;
Eif (root != null) {
previous_async = root.async;
}
async.noConflict = function () {
root.async = previous_async;
return async;
};
function only_once(fn) {
var called = false;
return function() {
if (called) throw new Error("Callback was already called.");
called = true;
fn.apply(root, arguments);
}
}
//// cross-browser compatiblity functions ////
var _toString = Object.prototype.toString;
var _isArray = Array.isArray || function (obj) {
return _toString.call(obj) === '[object Array]';
};
var _each = function (arr, iterator) {
for (var i = 0; i < arr.length; i += 1) {
iterator(arr[i], i, arr);
}
};
var _map = function (arr, iterator) {
if (arr.map) {
return arr.map(iterator);
}
var results = [];
_each(arr, function (x, i, a) {
results.push(iterator(x, i, a));
});
return results;
};
var _reduce = function (arr, iterator, memo) {
if (arr.reduce) {
return arr.reduce(iterator, memo);
}
_each(arr, function (x, i, a) {
memo = iterator(memo, x, i, a);
});
return memo;
};
var _keys = function (obj) {
if (Object.keys) {
return Object.keys(obj);
}
var keys = [];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
};
//// exported async module functions ////
//// nextTick implementation with browser-compatible fallback ////
Iif (typeof process === 'undefined' || !(process.nextTick)) {
if (typeof setImmediate === 'function') {
async.nextTick = function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
async.setImmediate = async.nextTick;
}
else {
async.nextTick = function (fn) {
setTimeout(fn, 0);
};
async.setImmediate = async.nextTick;
}
}
else {
async.nextTick = process.nextTick;
Eif (typeof setImmediate !== 'undefined') {
async.setImmediate = function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
}
else {
async.setImmediate = async.nextTick;
}
}
async.each = function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
_each(arr, function (x) {
iterator(x, only_once(done) );
});
function done(err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed >= arr.length) {
callback();
}
}
}
};
async.forEach = async.each;
async.eachSeries = function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
var iterate = function () {
iterator(arr[completed], function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed >= arr.length) {
callback();
}
else {
iterate();
}
}
});
};
iterate();
};
async.forEachSeries = async.eachSeries;
async.eachLimit = function (arr, limit, iterator, callback) {
var fn = _eachLimit(limit);
fn.apply(null, [arr, iterator, callback]);
};
async.forEachLimit = async.eachLimit;
var _eachLimit = function (limit) {
return function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length || limit <= 0) {
return callback();
}
var completed = 0;
var started = 0;
var running = 0;
(function replenish () {
if (completed >= arr.length) {
return callback();
}
while (running < limit && started < arr.length) {
started += 1;
running += 1;
iterator(arr[started - 1], function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
running -= 1;
if (completed >= arr.length) {
callback();
}
else {
replenish();
}
}
});
}
})();
};
};
var doParallel = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [async.each].concat(args));
};
};
var doParallelLimit = function(limit, fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [_eachLimit(limit)].concat(args));
};
};
var doSeries = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [async.eachSeries].concat(args));
};
};
var _asyncMap = function (eachfn, arr, iterator, callback) {
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
if (!callback) {
eachfn(arr, function (x, callback) {
iterator(x.value, function (err) {
callback(err);
});
});
} else {
var results = [];
eachfn(arr, function (x, callback) {
iterator(x.value, function (err, v) {
results[x.index] = v;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.map = doParallel(_asyncMap);
async.mapSeries = doSeries(_asyncMap);
async.mapLimit = function (arr, limit, iterator, callback) {
return _mapLimit(limit)(arr, iterator, callback);
};
var _mapLimit = function(limit) {
return doParallelLimit(limit, _asyncMap);
};
// reduce only has a series version, as doing reduce in parallel won't
// work in many situations.
async.reduce = function (arr, memo, iterator, callback) {
async.eachSeries(arr, function (x, callback) {
iterator(memo, x, function (err, v) {
memo = v;
callback(err);
});
}, function (err) {
callback(err, memo);
});
};
// inject alias
async.inject = async.reduce;
// foldl alias
async.foldl = async.reduce;
async.reduceRight = function (arr, memo, iterator, callback) {
var reversed = _map(arr, function (x) {
return x;
}).reverse();
async.reduce(reversed, memo, iterator, callback);
};
// foldr alias
async.foldr = async.reduceRight;
var _filter = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (v) {
if (v) {
results.push(x);
}
callback();
});
}, function (err) {
callback(_map(results.sort(function (a, b) {
return a.index - b.index;
}), function (x) {
return x.value;
}));
});
};
async.filter = doParallel(_filter);
async.filterSeries = doSeries(_filter);
// select alias
async.select = async.filter;
async.selectSeries = async.filterSeries;
var _reject = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (v) {
if (!v) {
results.push(x);
}
callback();
});
}, function (err) {
callback(_map(results.sort(function (a, b) {
return a.index - b.index;
}), function (x) {
return x.value;
}));
});
};
async.reject = doParallel(_reject);
async.rejectSeries = doSeries(_reject);
var _detect = function (eachfn, arr, iterator, main_callback) {
eachfn(arr, function (x, callback) {
iterator(x, function (result) {
if (result) {
main_callback(x);
main_callback = function () {};
}
else {
callback();
}
});
}, function (err) {
main_callback();
});
};
async.detect = doParallel(_detect);
async.detectSeries = doSeries(_detect);
async.some = function (arr, iterator, main_callback) {
async.each(arr, function (x, callback) {
iterator(x, function (v) {
if (v) {
main_callback(true);
main_callback = function () {};
}
callback();
});
}, function (err) {
main_callback(false);
});
};
// any alias
async.any = async.some;
async.every = function (arr, iterator, main_callback) {
async.each(arr, function (x, callback) {
iterator(x, function (v) {
if (!v) {
main_callback(false);
main_callback = function () {};
}
callback();
});
}, function (err) {
main_callback(true);
});
};
// all alias
async.all = async.every;
async.sortBy = function (arr, iterator, callback) {
async.map(arr, function (x, callback) {
iterator(x, function (err, criteria) {
if (err) {
callback(err);
}
else {
callback(null, {value: x, criteria: criteria});
}
});
}, function (err, results) {
if (err) {
return callback(err);
}
else {
var fn = function (left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
};
callback(null, _map(results.sort(fn), function (x) {
return x.value;
}));
}
});
};
async.auto = function (tasks, callback) {
callback = callback || function () {};
var keys = _keys(tasks);
var remainingTasks = keys.length
if (!remainingTasks) {
return callback();
}
var results = {};
var listeners = [];
var addListener = function (fn) {
listeners.unshift(fn);
};
var removeListener = function (fn) {
for (var i = 0; i < listeners.length; i += 1) {
if (listeners[i] === fn) {
listeners.splice(i, 1);
return;
}
}
};
var taskComplete = function () {
remainingTasks--
_each(listeners.slice(0), function (fn) {
fn();
});
};
addListener(function () {
if (!remainingTasks) {
var theCallback = callback;
// prevent final callback from calling itself if it errors
callback = function () {};
theCallback(null, results);
}
});
_each(keys, function (k) {
var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]];
var taskCallback = function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
if (err) {
var safeResults = {};
_each(_keys(results), function(rkey) {
safeResults[rkey] = results[rkey];
});
safeResults[k] = args;
callback(err, safeResults);
// stop subsequent errors hitting callback multiple times
callback = function () {};
}
else {
results[k] = args;
async.setImmediate(taskComplete);
}
};
var requires = task.slice(0, Math.abs(task.length - 1)) || [];
var ready = function () {
return _reduce(requires, function (a, x) {
return (a && results.hasOwnProperty(x));
}, true) && !results.hasOwnProperty(k);
};
if (ready()) {
task[task.length - 1](taskCallback, results);
}
else {
var listener = function () {
if (ready()) {
removeListener(listener);
task[task.length - 1](taskCallback, results);
}
};
addListener(listener);
}
});
};
async.retry = function(times, task, callback) {
var DEFAULT_TIMES = 5;
var attempts = [];
// Use defaults if times not passed
if (typeof times === 'function') {
callback = task;
task = times;
times = DEFAULT_TIMES;
}
// Make sure times is a number
times = parseInt(times, 10) || DEFAULT_TIMES;
var wrappedTask = function(wrappedCallback, wrappedResults) {
var retryAttempt = function(task, finalAttempt) {
return function(seriesCallback) {
task(function(err, result){
seriesCallback(!err || finalAttempt, {err: err, result: result});
}, wrappedResults);
};
};
while (times) {
attempts.push(retryAttempt(task, !(times-=1)));
}
async.series(attempts, function(done, data){
data = data[data.length - 1];
(wrappedCallback || callback)(data.err, data.result);
});
}
// If a callback is passed, run this as a controll flow
return callback ? wrappedTask() : wrappedTask
};
async.waterfall = function (tasks, callback) {
callback = callback || function () {};
if (!_isArray(tasks)) {
var err = new Error('First argument to waterfall must be an array of functions');
return callback(err);
}
if (!tasks.length) {
return callback();
}
var wrapIterator = function (iterator) {
return function (err) {
if (err) {
callback.apply(null, arguments);
callback = function () {};
}
else {
var args = Array.prototype.slice.call(arguments, 1);
var next = iterator.next();
if (next) {
args.push(wrapIterator(next));
}
else {
args.push(callback);
}
async.setImmediate(function () {
iterator.apply(null, args);
});
}
};
};
wrapIterator(async.iterator(tasks))();
};
var _parallel = function(eachfn, tasks, callback) {
callback = callback || function () {};
if (_isArray(tasks)) {
eachfn.map(tasks, function (fn, callback) {
if (fn) {
fn(function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
callback.call(null, err, args);
});
}
}, callback);
}
else {
var results = {};
eachfn.each(_keys(tasks), function (k, callback) {
tasks[k](function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
results[k] = args;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.parallel = function (tasks, callback) {
_parallel({ map: async.map, each: async.each }, tasks, callback);
};
async.parallelLimit = function(tasks, limit, callback) {
_parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback);
};
async.series = function (tasks, callback) {
callback = callback || function () {};
if (_isArray(tasks)) {
async.mapSeries(tasks, function (fn, callback) {
if (fn) {
fn(function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
callback.call(null, err, args);
});
}
}, callback);
}
else {
var results = {};
async.eachSeries(_keys(tasks), function (k, callback) {
tasks[k](function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
results[k] = args;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.iterator = function (tasks) {
var makeCallback = function (index) {
var fn = function () {
if (tasks.length) {
tasks[index].apply(null, arguments);
}
return fn.next();
};
fn.next = function () {
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
};
return fn;
};
return makeCallback(0);
};
async.apply = function (fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function () {
return fn.apply(
null, args.concat(Array.prototype.slice.call(arguments))
);
};
};
var _concat = function (eachfn, arr, fn, callback) {
var r = [];
eachfn(arr, function (x, cb) {
fn(x, function (err, y) {
r = r.concat(y || []);
cb(err);
});
}, function (err) {
callback(err, r);
});
};
async.concat = doParallel(_concat);
async.concatSeries = doSeries(_concat);
async.whilst = function (test, iterator, callback) {
if (test()) {
iterator(function (err) {
if (err) {
return callback(err);
}
async.whilst(test, iterator, callback);
});
}
else {
callback();
}
};
async.doWhilst = function (iterator, test, callback) {
iterator(function (err) {
if (err) {
return callback(err);
}
var args = Array.prototype.slice.call(arguments, 1);
if (test.apply(null, args)) {
async.doWhilst(iterator, test, callback);
}
else {
callback();
}
});
};
async.until = function (test, iterator, callback) {
if (!test()) {
iterator(function (err) {
if (err) {
return callback(err);
}
async.until(test, iterator, callback);
});
}
else {
callback();
}
};
async.doUntil = function (iterator, test, callback) {
iterator(function (err) {
if (err) {
return callback(err);
}
var args = Array.prototype.slice.call(arguments, 1);
if (!test.apply(null, args)) {
async.doUntil(iterator, test, callback);
}
else {
callback();
}
});
};
async.queue = function (worker, concurrency) {
if (concurrency === undefined) {
concurrency = 1;
}
function _insert(q, data, pos, callback) {
if (!q.started){
q.started = true;
}
if (!_isArray(data)) {
data = [data];
}
if(data.length == 0) {
// call drain immediately if there are no tasks
return async.setImmediate(function() {
if (q.drain) {
q.drain();
}
});
}
_each(data, function(task) {
var item = {
data: task,
callback: typeof callback === 'function' ? callback : null
};
if (pos) {
q.tasks.unshift(item);
} else {
q.tasks.push(item);
}
if (q.saturated && q.tasks.length === q.concurrency) {
q.saturated();
}
async.setImmediate(q.process);
});
}
var workers = 0;
var q = {
tasks: [],
concurrency: concurrency,
saturated: null,
empty: null,
drain: null,
started: false,
paused: false,
push: function (data, callback) {
_insert(q, data, false, callback);
},
kill: function () {
q.drain = null;
q.tasks = [];
},
unshift: function (data, callback) {
_insert(q, data, true, callback);
},
process: function () {
if (!q.paused && workers < q.concurrency && q.tasks.length) {
var task = q.tasks.shift();
if (q.empty && q.tasks.length === 0) {
q.empty();
}
workers += 1;
var next = function () {
workers -= 1;
if (task.callback) {
task.callback.apply(task, arguments);
}
if (q.drain && q.tasks.length + workers === 0) {
q.drain();
}
q.process();
};
var cb = only_once(next);
worker(task.data, cb);
}
},
length: function () {
return q.tasks.length;
},
running: function () {
return workers;
},
idle: function() {
return q.tasks.length + workers === 0;
},
pause: function () {
if (q.paused === true) { return; }
q.paused = true;
},
resume: function () {
if (q.paused === false) { return; }
q.paused = false;
// Need to call q.process once per concurrent
// worker to preserve full concurrency after pause
for (var w = 1; w <= q.concurrency; w++) {
async.setImmediate(q.process);
}
}
};
return q;
};
async.priorityQueue = function (worker, concurrency) {
function _compareTasks(a, b){
return a.priority - b.priority;
};
function _binarySearch(sequence, item, compare) {
var beg = -1,
end = sequence.length - 1;
while (beg < end) {
var mid = beg + ((end - beg + 1) >>> 1);
if (compare(item, sequence[mid]) >= 0) {
beg = mid;
} else {
end = mid - 1;
}
}
return beg;
}
function _insert(q, data, priority, callback) {
if (!q.started){
q.started = true;
}
if (!_isArray(data)) {
data = [data];
}
if(data.length == 0) {
// call drain immediately if there are no tasks
return async.setImmediate(function() {
if (q.drain) {
q.drain();
}
});
}
_each(data, function(task) {
var item = {
data: task,
priority: priority,
callback: typeof callback === 'function' ? callback : null
};
q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item);
if (q.saturated && q.tasks.length === q.concurrency) {
q.saturated();
}
async.setImmediate(q.process);
});
}
// Start with a normal queue
var q = async.queue(worker, concurrency);
// Override push to accept second parameter representing priority
q.push = function (data, priority, callback) {
_insert(q, data, priority, callback);
};
// Remove unshift function
delete q.unshift;
return q;
};
async.cargo = function (worker, payload) {
var working = false,
tasks = [];
var cargo = {
tasks: tasks,
payload: payload,
saturated: null,
empty: null,
drain: null,
drained: true,
push: function (data, callback) {
if (!_isArray(data)) {
data = [data];
}
_each(data, function(task) {
tasks.push({
data: task,
callback: typeof callback === 'function' ? callback : null
});
cargo.drained = false;
if (cargo.saturated && tasks.length === payload) {
cargo.saturated();
}
});
async.setImmediate(cargo.process);
},
process: function process() {
if (working) return;
if (tasks.length === 0) {
if(cargo.drain && !cargo.drained) cargo.drain();
cargo.drained = true;
return;
}
var ts = typeof payload === 'number'
? tasks.splice(0, payload)
: tasks.splice(0, tasks.length);
var ds = _map(ts, function (task) {
return task.data;
});
if(cargo.empty) cargo.empty();
working = true;
worker(ds, function () {
working = false;
var args = arguments;
_each(ts, function (data) {
if (data.callback) {
data.callback.apply(null, args);
}
});
process();
});
},
length: function () {
return tasks.length;
},
running: function () {
return working;
}
};
return cargo;
};
var _console_fn = function (name) {
return function (fn) {
var args = Array.prototype.slice.call(arguments, 1);
fn.apply(null, args.concat([function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (typeof console !== 'undefined') {
if (err) {
if (console.error) {
console.error(err);
}
}
else if (console[name]) {
_each(args, function (x) {
console[name](x);
});
}
}
}]));
};
};
async.log = _console_fn('log');
async.dir = _console_fn('dir');
/*async.info = _console_fn('info');
async.warn = _console_fn('warn');
async.error = _console_fn('error');*/
async.memoize = function (fn, hasher) {
var memo = {};
var queues = {};
hasher = hasher || function (x) {
return x;
};
var memoized = function () {
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
var key = hasher.apply(null, args);
if (key in memo) {
async.nextTick(function () {
callback.apply(null, memo[key]);
});
}
else if (key in queues) {
queues[key].push(callback);
}
else {
queues[key] = [callback];
fn.apply(null, args.concat([function () {
memo[key] = arguments;
var q = queues[key];
delete queues[key];
for (var i = 0, l = q.length; i < l; i++) {
q[i].apply(null, arguments);
}
}]));
}
};
memoized.memo = memo;
memoized.unmemoized = fn;
return memoized;
};
async.unmemoize = function (fn) {
return function () {
return (fn.unmemoized || fn).apply(null, arguments);
};
};
async.times = function (count, iterator, callback) {
var counter = [];
for (var i = 0; i < count; i++) {
counter.push(i);
}
return async.map(counter, iterator, callback);
};
async.timesSeries = function (count, iterator, callback) {
var counter = [];
for (var i = 0; i < count; i++) {
counter.push(i);
}
return async.mapSeries(counter, iterator, callback);
};
async.seq = function (/* functions... */) {
var fns = arguments;
return function () {
var that = this;
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
async.reduce(fns, args, function (newargs, fn, cb) {
fn.apply(that, newargs.concat([function () {
var err = arguments[0];
var nextargs = Array.prototype.slice.call(arguments, 1);
cb(err, nextargs);
}]))
},
function (err, results) {
callback.apply(that, [err].concat(results));
});
};
};
async.compose = function (/* functions... */) {
return async.seq.apply(null, Array.prototype.reverse.call(arguments));
};
var _applyEach = function (eachfn, fns /*args...*/) {
var go = function () {
var that = this;
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
return eachfn(fns, function (fn, cb) {
fn.apply(that, args.concat([cb]));
},
callback);
};
if (arguments.length > 2) {
var args = Array.prototype.slice.call(arguments, 2);
return go.apply(this, args);
}
else {
return go;
}
};
async.applyEach = doParallel(_applyEach);
async.applyEachSeries = doSeries(_applyEach);
async.forever = function (fn, callback) {
function next(err) {
if (err) {
if (callback) {
return callback(err);
}
throw err;
}
fn(next);
}
next();
};
// Node.js
Eif (typeof module !== 'undefined' && module.exports) {
module.exports = async;
}
// AMD / RequireJS
else if (typeof define !== 'undefined' && define.amd) {
define([], function () {
return async;
});
}
// included directly via <script> tag
else {
root.async = async;
}
}());
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| Gruntfile.js | 4.76% | (1 / 21) | 0% | (0 / 6) | 0% | (0 / 4) | 4.76% | (1 / 21) | |
| index.js | 100% | (8 / 8) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (8 / 8) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. // Node module: loopback // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT /* global module:false */ 'use strict'; module.exports = function(grunt) { // Do not report warnings from unit-tests exercising deprecated paths process.env.NO_DEPRECATION = 'loopback'; grunt.loadNpmTasks('grunt-mocha-test'); // Project configuration. grunt.initConfig({ // Metadata. pkg: grunt.file.readJSON('package.json'), banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', // Task configuration. uglify: { options: { banner: '<%= banner %>', }, dist: { files: { 'dist/loopback.min.js': ['dist/loopback.js'], }, }, }, eslint: { gruntfile: { src: 'Gruntfile.js', }, lib: { src: ['lib/**/*.js'], }, common: { src: ['common/**/*.js'], }, server: { src: ['server/**/*.js'], }, test: { src: ['test/**/*.js'], }, }, watch: { gruntfile: { files: '<%= eslint.gruntfile.src %>', tasks: ['eslint:gruntfile'], }, browser: { files: ['<%= eslint.browser.src %>'], tasks: ['eslint:browser'], }, common: { files: ['<%= eslint.common.src %>'], tasks: ['eslint:common'], }, lib: { files: ['<%= eslint.lib.src %>'], tasks: ['eslint:lib'], }, server: { files: ['<%= eslint.server.src %>'], tasks: ['eslint:server'], }, test: { files: ['<%= eslint.test.src %>'], tasks: ['eslint:test'], }, }, browserify: { dist: { files: { 'dist/loopback.js': ['index.js'], }, options: { ignore: ['nodemailer', 'passport', 'bcrypt'], standalone: 'loopback', }, }, }, mochaTest: { 'unit': { src: 'test/*.js', options: { reporter: 'dot', require: require.resolve('./test/helpers/use-english.js'), }, }, 'unit-xml': { src: 'test/*.js', options: { reporter: 'xunit', captureFile: 'xunit.xml', }, }, }, karma: { 'unit-once': { configFile: 'test/karma.conf.js', browsers: ['PhantomJS'], singleRun: true, reporters: ['dots', 'junit'], // increase the timeout for slow build slaves (e.g. Travis-ci) browserNoActivityTimeout: 30000, // CI friendly test output junitReporter: { outputFile: 'karma-xunit.xml', }, browserify: { // Disable sourcemaps to prevent // Fatal error: Maximum call stack size exceeded debug: false, // Disable watcher, grunt will exit after the first run watch: false, }, }, unit: { configFile: 'test/karma.conf.js', }, e2e: { options: { // base path, that will be used to resolve files and exclude basePath: '', // frameworks to use frameworks: ['mocha', 'browserify'], // list of files / patterns to load in the browser files: [ 'test/e2e/remote-connector.e2e.js', 'test/e2e/replication.e2e.js', ], // list of files to exclude exclude: [ ], // test results reporter to use // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' reporters: ['dots'], // web server port port: 9876, // cli runner port runnerPort: 9100, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: 'warn', // enable / disable watching file and executing tests whenever any file changes autoWatch: true, // Start these browsers, currently available: // - Chrome // - ChromeCanary // - Firefox // - Opera // - Safari (only Mac) // - PhantomJS // - IE (only Windows) browsers: [ 'Chrome', ], // If browser does not capture in given timeout [ms], kill it captureTimeout: 60000, // Continuous Integration mode // if true, it capture browsers, run tests and exit singleRun: false, // Browserify config (all optional) browserify: { // extensions: ['.coffee'], ignore: [ 'nodemailer', 'passport', 'passport-local', 'superagent', 'supertest', 'bcrypt', ], // transform: ['coffeeify'], // debug: true, // noParse: ['jquery'], watch: true, }, // Add browserify to preprocessors preprocessors: {'test/e2e/*': ['browserify']}, }, }, }, }); // These plugins provide necessary tasks. grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-eslint'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-karma'); grunt.registerTask('e2e-server', function() { var done = this.async(); var app = require('./test/fixtures/e2e/app'); app.listen(0, function() { process.env.PORT = this.address().port; done(); }); }); grunt.registerTask('skip-karma-on-windows', function() { console.log('*** SKIPPING PHANTOM-JS BASED TESTS ON WINDOWS ***'); }); grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']); // Default task. grunt.registerTask('default', ['browserify']); grunt.registerTask('test', [ 'eslint', process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit', process.env.JENKINS_HOME && /^win/.test(process.platform) ? 'skip-karma-on-windows' : 'karma:unit-once', ]); // alias for sl-ci-run and `npm test` grunt.registerTask('mocha-and-karma', ['test']); }; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/**
* loopback ~ public api
*/
const loopback = module.exports = require('./lib/loopback');
const datasourceJuggler = require('loopback-datasource-juggler');
/**
* Connectors
*/
loopback.Connector = require('./lib/connectors/base-connector');
loopback.Memory = require('./lib/connectors/memory');
loopback.Mail = require('./lib/connectors/mail');
loopback.Remote = require('loopback-connector-remote');
/**
* Types
*/
loopback.GeoPoint = require('loopback-datasource-juggler/lib/geo').GeoPoint;
loopback.ValidationError = loopback.Model.ValidationError;
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| access-token.js | 11.43% | (12 / 105) | 0% | (0 / 74) | 6.67% | (1 / 15) | 11.54% | (12 / 104) | |
| acl.js | 15.33% | (42 / 274) | 0% | (0 / 169) | 3.23% | (1 / 31) | 15.85% | (42 / 265) | |
| application.js | 14.29% | (10 / 70) | 0% | (0 / 32) | 11.11% | (1 / 9) | 14.49% | (10 / 69) | |
| change.js | 18.02% | (62 / 344) | 1.39% | (2 / 144) | 4.29% | (3 / 70) | 19.24% | (61 / 317) | |
| checkpoint.js | 26.09% | (6 / 23) | 0% | (0 / 4) | 12.5% | (1 / 8) | 28.57% | (6 / 21) | |
| email.js | 66.67% | (4 / 6) | 100% | (0 / 0) | 33.33% | (1 / 3) | 66.67% | (4 / 6) | |
| key-value-model.js | 33.33% | (11 / 33) | 0% | (0 / 2) | 10% | (1 / 10) | 34.38% | (11 / 32) | |
| role-mapping.js | 22.73% | (10 / 44) | 0% | (0 / 16) | 12.5% | (1 / 8) | 22.73% | (10 / 44) | |
| role.js | 12.24% | (35 / 286) | 0.95% | (2 / 210) | 4.08% | (2 / 49) | 13.06% | (35 / 268) | |
| scope.js | 31.25% | (5 / 16) | 0% | (0 / 6) | 25% | (1 / 4) | 33.33% | (5 / 15) | |
| user.js | 14.69% | (68 / 463) | 1.76% | (6 / 341) | 3.77% | (2 / 53) | 15.18% | (68 / 448) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module Dependencies.
*/
'use strict';
var g = require('../../lib/globalize');
var loopback = require('../../lib/loopback');
var assert = require('assert');
var uid = require('uid2');
var DEFAULT_TOKEN_LEN = 64;
/**
* Token based authentication and access control.
*
* **Default ACLs**
*
* - DENY EVERYONE `*`
* - ALLOW EVERYONE create
*
* @property {String} id Generated token ID.
* @property {Number} ttl Time to live in seconds, 2 weeks by default.
* @property {Date} created When the token was created.
* @property {Object} settings Extends the `Model.settings` object.
* @property {Number} settings.accessTokenIdLength Length of the base64-encoded string access token. Default value is 64.
* Increase the length for a more secure access token.
*
* @class AccessToken
* @inherits {PersistedModel}
*/
module.exports = function(AccessToken) {
/**
* Anonymous Token
*
* ```js
* assert(AccessToken.ANONYMOUS.id === '$anonymous');
* ```
*/
AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
/**
* Create a cryptographically random access token id.
*
* @callback {Function} callback
* @param {Error} err
* @param {String} token
*/
AccessToken.createAccessTokenId = function(fn) {
uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
if (err) {
fn(err);
} else {
fn(null, guid);
}
});
};
/*!
* Hook to create accessToken id.
*/
AccessToken.observe('before save', function(ctx, next) {
if (!ctx.instance || ctx.instance.id) {
// We are running a partial update or the instance already has an id
return next();
}
AccessToken.createAccessTokenId(function(err, id) {
if (err) return next(err);
ctx.instance.id = id;
next();
});
});
/**
* Find a token for the given `ServerRequest`.
*
* @param {ServerRequest} req
* @param {Object} [options] Options for finding the token
* @callback {Function} callback
* @param {Error} err
* @param {AccessToken} token
*/
AccessToken.findForRequest = function(req, options, cb) {
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
var id = tokenIdForRequest(req, options);
if (id) {
this.findById(id, function(err, token) {
if (err) {
cb(err);
} else if (token) {
token.validate(function(err, isValid) {
if (err) {
cb(err);
} else if (isValid) {
cb(null, token);
} else {
var e = new Error(g.f('Invalid Access Token'));
e.status = e.statusCode = 401;
e.code = 'INVALID_TOKEN';
cb(e);
}
});
} else {
cb();
}
});
} else {
process.nextTick(function() {
cb();
});
}
};
/**
* Validate the token.
*
* @callback {Function} callback
* @param {Error} err
* @param {Boolean} isValid
*/
AccessToken.prototype.validate = function(cb) {
try {
assert(
this.created && typeof this.created.getTime === 'function',
'token.created must be a valid Date'
);
assert(this.ttl !== 0, 'token.ttl must be not be 0');
assert(this.ttl, 'token.ttl must exist');
assert(this.ttl >= -1, 'token.ttl must be >= -1');
var AccessToken = this.constructor;
var userRelation = AccessToken.relations.user; // may not be set up
var User = userRelation && userRelation.modelTo;
// redefine user model if accessToken's principalType is available
if (this.principalType) {
User = AccessToken.registry.findModel(this.principalType);
if (!User) {
process.nextTick(function() {
return cb(null, false);
});
}
}
var now = Date.now();
var created = this.created.getTime();
var elapsedSeconds = (now - created) / 1000;
var secondsToLive = this.ttl;
var eternalTokensAllowed = !!(User && User.settings.allowEternalTokens);
var isEternalToken = secondsToLive === -1;
var isValid = isEternalToken ?
eternalTokensAllowed :
elapsedSeconds < secondsToLive;
if (isValid) {
process.nextTick(function() {
cb(null, isValid);
});
} else {
this.destroy(function(err) {
cb(err, isValid);
});
}
} catch (e) {
process.nextTick(function() {
cb(e);
});
}
};
function tokenIdForRequest(req, options) {
var params = options.params || [];
var headers = options.headers || [];
var cookies = options.cookies || [];
var i = 0;
var length, id;
// https://github.com/strongloop/loopback/issues/1326
if (options.searchDefaultTokenKeys !== false) {
params = params.concat(['access_token']);
headers = headers.concat(['X-Access-Token', 'authorization']);
cookies = cookies.concat(['access_token', 'authorization']);
}
for (length = params.length; i < length; i++) {
var param = params[i];
// replacement for deprecated req.param()
id = req.params && req.params[param] !== undefined ? req.params[param] :
req.body && req.body[param] !== undefined ? req.body[param] :
req.query && req.query[param] !== undefined ? req.query[param] :
undefined;
if (typeof id === 'string') {
return id;
}
}
for (i = 0, length = headers.length; i < length; i++) {
id = req.header(headers[i]);
if (typeof id === 'string') {
// Add support for oAuth 2.0 bearer token
// http://tools.ietf.org/html/rfc6750
if (id.indexOf('Bearer ') === 0) {
id = id.substring(7);
// Decode from base64
var buf = new Buffer(id, 'base64');
id = buf.toString('utf8');
} else if (/^Basic /i.test(id)) {
id = id.substring(6);
id = (new Buffer(id, 'base64')).toString('utf8');
// The spec says the string is user:pass, so if we see both parts
// we will assume the longer of the two is the token, so we will
// extract "a2b2c3" from:
// "a2b2c3"
// "a2b2c3:" (curl http://a2b2c3@localhost:3000/)
// "token:a2b2c3" (curl http://token:a2b2c3@localhost:3000/)
// ":a2b2c3"
var parts = /^([^:]*):(.*)$/.exec(id);
if (parts) {
id = parts[2].length > parts[1].length ? parts[2] : parts[1];
}
}
return id;
}
}
if (req.signedCookies) {
for (i = 0, length = cookies.length; i < length; i++) {
id = req.signedCookies[cookies[i]];
if (typeof id === 'string') {
return id;
}
}
}
return null;
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
/*!
Schema ACL options
Object level permissions, for example, an album owned by a user
Factors to be authorized against:
* model name: Album
* model instance properties: userId of the album, friends, shared
* methods
* app and/or user ids/roles
** loggedIn
** roles
** userId
** appId
** none
** everyone
** relations: owner/friend/granted
Class level permissions, for example, Album
* model name: Album
* methods
URL/Route level permissions
* url pattern
* application id
* ip addresses
* http headers
Map to oAuth 2.0 scopes
*/
var g = require('../../lib/globalize');
var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils');
var async = require('async');
var extend = require('util')._extend;
var assert = require('assert');
var debug = require('debug')('loopback:security:acl');
var ctx = require('../../lib/access-context');
var AccessContext = ctx.AccessContext;
var Principal = ctx.Principal;
var AccessRequest = ctx.AccessRequest;
var Role = loopback.Role;
assert(Role, 'Role model must be defined before ACL model');
/**
* A Model for access control meta data.
*
* System grants permissions to principals (users/applications, can be grouped
* into roles).
*
* Protected resource: the model data and operations
* (model/property/method/relation/…)
*
* For a given principal, such as client application and/or user, is it allowed
* to access (read/write/execute)
* the protected resource?
*
* @header ACL
* @property {String} model Name of the model.
* @property {String} property Name of the property, method, scope, or relation.
* @property {String} accessType Type of access being granted: one of READ, WRITE, or EXECUTE.
* @property {String} permission Type of permission granted. One of:
*
* - ALARM: Generate an alarm, in a system-dependent way, the access specified in the permissions component of the ACL entry.
* - ALLOW: Explicitly grants access to the resource.
* - AUDIT: Log, in a system-dependent way, the access specified in the permissions component of the ACL entry.
* - DENY: Explicitly denies access to the resource.
* @property {String} principalType Type of the principal; one of: Application, User, Role.
* @property {String} principalId ID of the principal - such as appId, userId or roleId.
* @property {Object} settings Extends the `Model.settings` object.
* @property {String} settings.defaultPermission Default permission setting: ALLOW, DENY, ALARM, or AUDIT. Default is ALLOW.
* Set to DENY to prohibit all API access by default.
*
* @class ACL
* @inherits PersistedModel
*/
module.exports = function(ACL) {
ACL.ALL = AccessContext.ALL;
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
ACL.ALLOW = AccessContext.ALLOW; // Allow
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
ACL.DENY = AccessContext.DENY; // Deny
ACL.READ = AccessContext.READ; // Read operation
ACL.REPLICATE = AccessContext.REPLICATE; // Replicate (pull) changes
ACL.WRITE = AccessContext.WRITE; // Write operation
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
ACL.USER = Principal.USER;
ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
ACL.ROLE = Principal.ROLE;
ACL.SCOPE = Principal.SCOPE;
/**
* Calculate the matching score for the given rule and request
* @param {ACL} rule The ACL entry
* @param {AccessRequest} req The request
* @returns {Number}
*/
ACL.getMatchingScore = function getMatchingScore(rule, req) {
var props = ['model', 'property', 'accessType'];
var score = 0;
for (var i = 0; i < props.length; i++) {
// Shift the score by 4 for each of the properties as the weight
score = score * 4;
var ruleValue = rule[props[i]] || ACL.ALL;
var requestedValue = req[props[i]] || ACL.ALL;
var isMatchingMethodName = props[i] === 'property' &&
req.methodNames.indexOf(ruleValue) !== -1;
var isMatchingAccessType = ruleValue === requestedValue;
if (props[i] === 'accessType' && !isMatchingAccessType) {
switch (ruleValue) {
case ACL.EXECUTE:
// EXECUTE should match READ, REPLICATE and WRITE
isMatchingAccessType = true;
break;
case ACL.WRITE:
// WRITE should match REPLICATE too
isMatchingAccessType = requestedValue === ACL.REPLICATE;
break;
}
}
if (isMatchingMethodName || isMatchingAccessType) {
// Exact match
score += 3;
} else if (ruleValue === ACL.ALL) {
// Wildcard match
score += 2;
} else if (requestedValue === ACL.ALL) {
score += 1;
} else {
// Doesn't match at all
return -1;
}
}
// Weigh against the principal type into 4 levels
// - user level (explicitly allow/deny a given user)
// - app level (explicitly allow/deny a given app)
// - role level (role based authorization)
// - other
// user > app > role > ...
score = score * 4;
switch (rule.principalType) {
case ACL.USER:
score += 4;
break;
case ACL.APP:
score += 3;
break;
case ACL.ROLE:
score += 2;
break;
default:
score += 1;
}
// Weigh against the roles
// everyone < authenticated/unauthenticated < related < owner < ...
score = score * 8;
if (rule.principalType === ACL.ROLE) {
switch (rule.principalId) {
case Role.OWNER:
score += 4;
break;
case Role.RELATED:
score += 3;
break;
case Role.AUTHENTICATED:
case Role.UNAUTHENTICATED:
score += 2;
break;
case Role.EVERYONE:
score += 1;
break;
default:
score += 5;
}
}
score = score * 4;
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
return score;
};
/**
* Get matching score for the given `AccessRequest`.
* @param {AccessRequest} req The request
* @returns {Number} score
*/
ACL.prototype.score = function(req) {
return this.constructor.getMatchingScore(this, req);
};
/*!
* Resolve permission from the ACLs
* @param {Object[]) acls The list of ACLs
* @param {AccessRequest} req The access request
* @returns {AccessRequest} result The resolved access request
*/
ACL.resolvePermission = function resolvePermission(acls, req) {
if (!(req instanceof AccessRequest)) {
req.registry = this.registry;
req = new AccessRequest(req);
}
// Sort by the matching score in descending order
acls = acls.sort(function(rule1, rule2) {
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
});
var permission = ACL.DEFAULT;
var score = 0;
for (var i = 0; i < acls.length; i++) {
var candidate = acls[i];
score = ACL.getMatchingScore(candidate, req);
if (score < 0) {
// the highest scored ACL did not match
break;
}
if (!req.isWildcard()) {
// We should stop from the first match for non-wildcard
permission = candidate.permission;
break;
} else {
if (req.exactlyMatches(candidate)) {
permission = candidate.permission;
break;
}
// For wildcard match, find the strongest permission
var candidateOrder = AccessContext.permissionOrder[candidate.permission];
var permissionOrder = AccessContext.permissionOrder[permission];
if (candidateOrder > permissionOrder) {
permission = candidate.permission;
}
}
}
if (debug.enabled) {
debug('The following ACLs were searched: ');
acls.forEach(function(acl) {
acl.debug();
debug('with score:', acl.score(req));
});
}
var res = new AccessRequest({
model: req.model,
property: req.property,
accessType: req.accessType,
permission: permission || ACL.DEFAULT,
registry: this.registry});
// Elucidate permission status if DEFAULT
res.settleDefaultPermission();
return res;
};
/*!
* Get the static ACLs from the model definition
* @param {String} model The model name
* @param {String} property The property/method/relation name
*
* @return {Object[]} An array of ACLs
*/
ACL.getStaticACLs = function getStaticACLs(model, property) {
var modelClass = this.registry.findModel(model);
var staticACLs = [];
if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function(acl) {
var prop = acl.property;
// We support static ACL property with array of string values.
if (Array.isArray(prop) && prop.indexOf(property) >= 0)
prop = property;
if (!prop || prop === ACL.ALL || property === prop) {
staticACLs.push(new ACL({
model: model,
property: prop || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType || ACL.ALL,
permission: acl.permission,
}));
}
});
}
var prop = modelClass && (
// regular property
modelClass.definition.properties[property] ||
// relation/scope
(modelClass._scopeMeta && modelClass._scopeMeta[property]) ||
// static method
modelClass[property] ||
// prototype method
modelClass.prototype[property]);
if (prop && prop.acls) {
prop.acls.forEach(function(acl) {
staticACLs.push(new ACL({
model: modelClass.modelName,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission,
}));
});
}
return staticACLs;
};
/**
* Check if the given principal is allowed to access the model/property
* @param {String} principalType The principal type.
* @param {String} principalId The principal ID.
* @param {String} model The model name.
* @param {String} property The property/method/relation name.
* @param {String} accessType The access type.
* @callback {Function} callback Callback function.
* @param {String|Error} err The error object.
* @param {AccessRequest} result The resolved access request.
*/
ACL.checkPermission = function checkPermission(principalType, principalId,
model, property, accessType,
callback) {
if (!callback) callback = utils.createPromiseCallback();
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
principalId = principalId.toString();
}
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined :
{inq: [accessType, ACL.ALL, ACL.EXECUTE]};
var req = new AccessRequest({model, property, accessType, registry: this.registry});
var acls = this.getStaticACLs(model, property);
// resolved is an instance of AccessRequest
var resolved = this.resolvePermission(acls, req);
if (resolved && resolved.permission === ACL.DENY) {
debug('Permission denied by statically resolved permission');
debug(' Resolved Permission: %j', resolved);
process.nextTick(function() {
callback(null, resolved);
});
return callback.promise;
}
var self = this;
this.find({where: {principalType: principalType, principalId: principalId,
model: model, property: propertyQuery, accessType: accessTypeQuery}},
function(err, dynACLs) {
if (err) {
return callback(err);
}
acls = acls.concat(dynACLs);
// resolved is an instance of AccessRequest
resolved = self.resolvePermission(acls, req);
return callback(null, resolved);
});
return callback.promise;
};
ACL.prototype.debug = function() {
if (debug.enabled) {
debug('---ACL---');
debug('model %s', this.model);
debug('property %s', this.property);
debug('principalType %s', this.principalType);
debug('principalId %s', this.principalId);
debug('accessType %s', this.accessType);
debug('permission %s', this.permission);
}
};
// NOTE Regarding ACL.isAllowed() and ACL.prototype.isAllowed()
// Extending existing logic, including from ACL.checkAccessForContext() method,
// ACL instance with missing property `permission` are not promoted to
// permission = ACL.DEFAULT config. Such ACL instances will hence always be
// inefective
/**
* Test if ACL's permission is ALLOW
* @param {String} permission The permission to test, expects one of 'ALLOW', 'DENY', 'DEFAULT'
* @param {String} defaultPermission The default permission to apply if not providing a finite one in the permission parameter
* @returns {Boolean} true if ACL permission is ALLOW
*/
ACL.isAllowed = function(permission, defaultPermission) {
if (permission === ACL.DEFAULT) {
permission = defaultPermission || ACL.ALLOW;
}
return permission !== loopback.ACL.DENY;
};
/**
* Test if ACL's permission is ALLOW
* @param {String} defaultPermission The default permission to apply if missing in ACL instance
* @returns {Boolean} true if ACL permission is ALLOW
*/
ACL.prototype.isAllowed = function(defaultPermission) {
return this.constructor.isAllowed(this.permission, defaultPermission);
};
/**
* Check if the request has the permission to access.
* @options {AccessContext|Object} context
* An AccessContext instance or a plain object with the following properties.
* @property {Object[]} principals An array of principals.
* @property {String|Model} model The model name or model class.
* @property {*} modelId The model instance ID.
* @property {String} property The property/method/relation name.
* @property {String} accessType The access type:
* READ, REPLICATE, WRITE, or EXECUTE.
* @callback {Function} callback Callback function
* @param {String|Error} err The error object.
* @param {AccessRequest} result The resolved access request.
*/
ACL.checkAccessForContext = function(context, callback) {
if (!callback) callback = utils.createPromiseCallback();
var self = this;
self.resolveRelatedModels();
var roleModel = self.roleModel;
if (!(context instanceof AccessContext)) {
context.registry = this.registry;
context = new AccessContext(context);
}
var authorizedRoles = {};
var remotingContext = context.remotingContext;
var model = context.model;
var modelDefaultPermission = model && model.settings.defaultPermission;
var property = context.property;
var accessType = context.accessType;
var modelName = context.modelName;
var methodNames = context.methodNames;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
var accessTypeQuery = (accessType === ACL.ALL) ?
undefined :
(accessType === ACL.REPLICATE) ?
{inq: [ACL.REPLICATE, ACL.WRITE, ACL.ALL]} :
{inq: [accessType, ACL.ALL]};
var req = new AccessRequest({
model: modelName,
property,
accessType,
permission: ACL.DEFAULT,
methodNames,
registry: this.registry});
var effectiveACLs = [];
var staticACLs = self.getStaticACLs(model.modelName, property);
this.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function(err, acls) {
if (err) return callback(err);
var inRoleTasks = [];
acls = acls.concat(staticACLs);
acls.forEach(function(acl) {
// Check exact matches
for (var i = 0; i < context.principals.length; i++) {
var p = context.principals[i];
var typeMatch = p.type === acl.principalType;
var idMatch = String(p.id) === String(acl.principalId);
if (typeMatch && idMatch) {
effectiveACLs.push(acl);
return;
}
}
// Check role matches
if (acl.principalType === ACL.ROLE) {
inRoleTasks.push(function(done) {
roleModel.isInRole(acl.principalId, context,
function(err, inRole) {
if (!err && inRole) {
effectiveACLs.push(acl);
// add the role to authorizedRoles if allowed
if (acl.isAllowed(modelDefaultPermission))
authorizedRoles[acl.principalId] = true;
}
done(err, acl);
});
});
}
});
async.parallel(inRoleTasks, function(err, results) {
if (err) return callback(err, null);
// resolved is an instance of AccessRequest
var resolved = self.resolvePermission(effectiveACLs, req);
debug('---Resolved---');
resolved.debug();
// set authorizedRoles in remotingContext options argument if
// resolved AccessRequest permission is ALLOW, else set it to empty object
authorizedRoles = resolved.isAllowed() ? authorizedRoles : {};
saveAuthorizedRolesToRemotingContext(remotingContext, authorizedRoles);
return callback(null, resolved);
});
});
return callback.promise;
};
function saveAuthorizedRolesToRemotingContext(remotingContext, authorizedRoles) {
const options = remotingContext && remotingContext.args && remotingContext.args.options;
// authorizedRoles key/value map is added to the options argument only if
// the latter exists and is an object. This means that the feature's availability
// will depend on the app configuration
if (options && typeof options === 'object') { // null is object too
options.authorizedRoles = authorizedRoles;
}
}
/**
* Check if the given access token can invoke the method
* @param {AccessToken} token The access token
* @param {String} model The model name
* @param {*} modelId The model id
* @param {String} method The method name
* @callback {Function} callback Callback function
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
assert(token, 'Access token is required');
if (!callback) callback = utils.createPromiseCallback();
var context = new AccessContext({
registry: this.registry,
accessToken: token,
model: model,
property: method,
method: method,
modelId: modelId,
});
this.checkAccessForContext(context, function(err, accessRequest) {
if (err) callback(err);
else callback(null, accessRequest.isAllowed());
});
return callback.promise;
};
ACL.resolveRelatedModels = function() {
if (!this.roleModel) {
var reg = this.registry;
this.roleModel = reg.getModelByType('Role');
this.roleMappingModel = reg.getModelByType('RoleMapping');
this.userModel = reg.getModelByType('User');
this.applicationModel = reg.getModelByType('Application');
}
};
/**
* Resolve a principal by type/id
* @param {String} type Principal type - ROLE/APP/USER
* @param {String|Number} id Principal id or name
* @callback {Function} callback Callback function
* @param {String|Error} err The error object
* @param {Object} result An instance of principal (Role, Application or User)
*/
ACL.resolvePrincipal = function(type, id, cb) {
cb = cb || utils.createPromiseCallback();
type = type || ACL.ROLE;
this.resolveRelatedModels();
switch (type) {
case ACL.ROLE:
this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb);
break;
case ACL.USER:
this.userModel.findOne(
{where: {or: [{username: id}, {email: id}, {id: id}]}}, cb);
break;
case ACL.APP:
this.applicationModel.findOne(
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb);
break;
default:
// try resolving a user model with a name matching the principalType
var userModel = this.registry.findModel(type);
if (userModel) {
userModel.findOne(
{where: {or: [{username: id}, {email: id}, {id: id}]}},
cb);
} else {
process.nextTick(function() {
var err = new Error(g.f('Invalid principal type: %s', type));
err.statusCode = 400;
err.code = 'INVALID_PRINCIPAL_TYPE';
cb(err);
});
}
}
return cb.promise;
};
/**
* Check if the given principal is mapped to the role
* @param {String} principalType Principal type
* @param {String|*} principalId Principal id/name
* @param {String|*} role Role id/name
* @callback {Function} callback Callback function
* @param {String|Error} err The error object
* @param {Boolean} isMapped is the ACL mapped to the role
*/
ACL.isMappedToRole = function(principalType, principalId, role, cb) {
cb = cb || utils.createPromiseCallback();
var self = this;
this.resolvePrincipal(principalType, principalId,
function(err, principal) {
if (err) return cb(err);
if (principal != null) {
principalId = principal.id;
}
principalType = principalType || 'ROLE';
self.resolvePrincipal('ROLE', role, function(err, role) {
if (err || !role) return cb(err, role);
self.roleMappingModel.findOne({
where: {
roleId: role.id,
principalType: principalType,
principalId: String(principalId),
},
}, function(err, result) {
if (err) return cb(err);
return cb(null, !!result);
});
});
});
return cb.promise;
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var utils = require('../../lib/utils');
/*!
* Application management functions
*/
var crypto = require('crypto');
function generateKey(hmacKey, algorithm, encoding) {
hmacKey = hmacKey || 'loopback';
algorithm = algorithm || 'sha1';
encoding = encoding || 'hex';
var hmac = crypto.createHmac(algorithm, hmacKey);
var buf = crypto.randomBytes(32);
hmac.update(buf);
var key = hmac.digest(encoding);
return key;
}
/**
* Manage client applications and organize their users.
*
* @property {String} id Generated ID.
* @property {String} name Name; required.
* @property {String} description Text description
* @property {String} icon String Icon image URL.
* @property {String} owner User ID of the developer who registers the application.
* @property {String} email E-mail address
* @property {Boolean} emailVerified Whether the e-mail is verified.
* @property {String} url OAuth 2.0 application URL.
* @property {String}[] callbackUrls The OAuth 2.0 code/token callback URL.
* @property {String} status Status of the application; Either `production`, `sandbox` (default), or `disabled`.
* @property {Date} created Date Application object was created. Default: current date.
* @property {Date} modified Date Application object was modified. Default: current date.
*
* @property {Object} pushSettings.apns APNS configuration, see the options
* below and also
* https://github.com/argon/node-apn/blob/master/doc/apn.markdown
* @property {Boolean} pushSettings.apns.production Whether to use production Apple Push Notification Service (APNS) servers to send push notifications.
* If true, uses `gateway.push.apple.com:2195` and `feedback.push.apple.com:2196`.
* If false, uses `gateway.sandbox.push.apple.com:2195` and `feedback.sandbox.push.apple.com:2196`
* @property {String} pushSettings.apns.certData The certificate data loaded from the cert.pem file (APNS).
* @property {String} pushSettings.apns.keyData The key data loaded from the key.pem file (APNS).
* @property {String} pushSettings.apns.pushOptions.gateway (APNS).
* @property {Number} pushSettings.apns.pushOptions.port (APNS).
* @property {String} pushSettings.apns.feedbackOptions.gateway (APNS).
* @property {Number} pushSettings.apns.feedbackOptions.port (APNS).
* @property {Boolean} pushSettings.apns.feedbackOptions.batchFeedback (APNS).
* @property {Number} pushSettings.apns.feedbackOptions.interval (APNS).
* @property {String} pushSettings.gcm.serverApiKey: Google Cloud Messaging API key.
*
* @property {Boolean} authenticationEnabled
* @property {Boolean} anonymousAllowed
* @property {Array} authenticationSchemes List of authentication schemes
* (see below).
* @property {String} authenticationSchemes.scheme Scheme name.
* Supported values: `local`, `facebook`, `google`,
* `twitter`, `linkedin`, `github`.
* @property {Object} authenticationSchemes.credential
* Scheme-specific credentials.
*
* @class Application
* @inherits {PersistedModel}
*/
module.exports = function(Application) {
/*!
* A hook to generate keys before creation
* @param next
*/
Application.observe('before save', function(ctx, next) {
if (!ctx.instance) {
// Partial update - don't generate new keys
// NOTE(bajtos) This also means that an atomic updateOrCreate
// will not generate keys when a new record is creatd
return next();
}
var app = ctx.instance;
app.created = app.modified = new Date();
if (!app.id) {
app.id = generateKey('id', 'md5');
}
app.clientKey = generateKey('client');
app.javaScriptKey = generateKey('javaScript');
app.restApiKey = generateKey('restApi');
app.windowsKey = generateKey('windows');
app.masterKey = generateKey('master');
next();
});
/**
* Register a new application
* @param {String} owner Owner's user ID.
* @param {String} name Name of the application
* @param {Object} options Other options
* @callback {Function} callback Callback function
* @param {Error} err
* @promise
*/
Application.register = function(owner, name, options, cb) {
assert(owner, 'owner is required');
assert(name, 'name is required');
if (typeof options === 'function' && !cb) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var props = {owner: owner, name: name};
for (var p in options) {
if (!(p in props)) {
props[p] = options[p];
}
}
this.create(props, cb);
return cb.promise;
};
/**
* Reset keys for the application instance
* @callback {Function} callback
* @param {Error} err
*/
Application.prototype.resetKeys = function(cb) {
this.clientKey = generateKey('client');
this.javaScriptKey = generateKey('javaScript');
this.restApiKey = generateKey('restApi');
this.windowsKey = generateKey('windows');
this.masterKey = generateKey('master');
this.modified = new Date();
this.save(cb);
};
/**
* Reset keys for a given application by the appId
* @param {Any} appId
* @callback {Function} callback
* @param {Error} err
* @promise
*/
Application.resetKeys = function(appId, cb) {
cb = cb || utils.createPromiseCallback();
this.findById(appId, function(err, app) {
if (err) {
if (cb) cb(err, app);
return;
}
app.resetKeys(cb);
});
return cb.promise;
};
/**
* Authenticate the application id and key.
*
* @param {Any} appId
* @param {String} key
* @callback {Function} callback
* @param {Error} err
* @param {String} matched The matching key; one of:
* - clientKey
* - javaScriptKey
* - restApiKey
* - windowsKey
* - masterKey
* @promise
*/
Application.authenticate = function(appId, key, cb) {
cb = cb || utils.createPromiseCallback();
this.findById(appId, function(err, app) {
if (err || !app) {
cb(err, null);
return cb.promise;
}
var result = null;
var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
for (var i = 0; i < keyNames.length; i++) {
if (app[keyNames[i]] === key) {
result = {
application: app,
keyType: keyNames[i],
};
break;
}
}
cb(null, result);
});
return cb.promise;
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module Dependencies.
*/
'use strict';
var g = require('../../lib/globalize');
var PersistedModel = require('../../lib/loopback').PersistedModel;
var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils');
var crypto = require('crypto');
var CJSON = {stringify: require('canonical-json')};
var async = require('async');
var assert = require('assert');
var debug = require('debug')('loopback:change');
/**
* Change list entry.
*
* @property {String} id Hash of the modelName and ID.
* @property {String} rev The current model revision.
* @property {String} prev The previous model revision.
* @property {Number} checkpoint The current checkpoint at time of the change.
* @property {String} modelName Model name.
* @property {String} modelId Model ID.
* @property {Object} settings Extends the `Model.settings` object.
* @property {String} settings.hashAlgorithm Algorithm used to create cryptographic hash, used as argument
* to [crypto.createHash](http://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm). Default is sha1.
* @property {Boolean} settings.ignoreErrors By default, when changes are rectified, an error will throw an exception.
* However, if this setting is true, then errors will not throw exceptions.
* @class Change
* @inherits {PersistedModel}
*/
module.exports = function(Change) {
/*!
* Constants
*/
Change.UPDATE = 'update';
Change.CREATE = 'create';
Change.DELETE = 'delete';
Change.UNKNOWN = 'unknown';
/*!
* Conflict Class
*/
Change.Conflict = Conflict;
/*!
* Setup the extended model.
*/
Change.setup = function() {
PersistedModel.setup.call(this);
var Change = this;
Change.getter.id = function() {
var hasModel = this.modelName && this.modelId;
Eif (!hasModel) return null;
return Change.idForModel(this.modelName, this.modelId);
};
};
Change.setup();
/**
* Track the recent change of the given modelIds.
*
* @param {String} modelName
* @param {Array} modelIds
* @callback {Function} callback
* @param {Error} err
* @param {Array} changes Changes that were tracked
*/
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
var Change = this;
var errors = [];
callback = callback || utils.createPromiseCallback();
var tasks = modelIds.map(function(id) {
return function(cb) {
Change.findOrCreateChange(modelName, id, function(err, change) {
if (err) return next(err);
change.rectify(next);
});
function next(err) {
if (err) {
err.modelName = modelName;
err.modelId = id;
errors.push(err);
}
cb();
}
};
});
async.parallel(tasks, function(err) {
if (err) return callback(err);
if (errors.length) {
var desc = errors
.map(function(e) {
return '#' + e.modelId + ' - ' + e.toString();
})
.join('\n');
var msg = g.f('Cannot rectify %s changes:\n%s', modelName, desc);
err = new Error(msg);
err.details = {errors: errors};
return callback(err);
}
callback();
});
return callback.promise;
};
/**
* Get an identifier for a given model.
*
* @param {String} modelName
* @param {String} modelId
* @return {String}
*/
Change.idForModel = function(modelName, modelId) {
return this.hash([modelName, modelId].join('-'));
};
/**
* Find or create a change for the given model.
*
* @param {String} modelName
* @param {String} modelId
* @callback {Function} callback
* @param {Error} err
* @param {Change} change
* @end
*/
Change.findOrCreateChange = function(modelName, modelId, callback) {
assert(this.registry.findModel(modelName), modelName + ' does not exist');
callback = callback || utils.createPromiseCallback();
var id = this.idForModel(modelName, modelId);
var Change = this;
this.findById(id, function(err, change) {
if (err) return callback(err);
if (change) {
callback(null, change);
} else {
var ch = new Change({
id: id,
modelName: modelName,
modelId: modelId,
});
ch.debug('creating change');
Change.updateOrCreate(ch, callback);
}
});
return callback.promise;
};
/**
* Update (or create) the change with the current revision.
*
* @callback {Function} callback
* @param {Error} err
* @param {Change} change
*/
Change.prototype.rectify = function(cb) {
var change = this;
var currentRev = this.rev;
change.debug('rectify change');
cb = cb || utils.createPromiseCallback();
const model = this.getModelCtor();
const id = this.getModelId();
model.findById(id, function(err, inst) {
if (err) return cb(err);
if (inst) {
inst.fillCustomChangeProperties(change, function() {
const rev = Change.revisionForInst(inst);
prepareAndDoRectify(rev);
});
} else {
prepareAndDoRectify(null);
}
});
return cb.promise;
function prepareAndDoRectify(rev) {
// avoid setting rev and prev to the same value
if (currentRev === rev) {
change.debug('rev and prev are equal (not updating anything)');
return cb(null, change);
}
// FIXME(@bajtos) Allow callers to pass in the checkpoint value
// (or even better - a memoized async function to get the cp value)
// That will enable `rectifyAll` to cache the checkpoint value
change.constructor.getCheckpointModel().current(
function(err, checkpoint) {
if (err) return cb(err);
doRectify(checkpoint, rev);
}
);
}
function doRectify(checkpoint, rev) {
if (rev) {
if (currentRev === rev) {
change.debug('ASSERTION FAILED: Change currentRev==rev ' +
'should have been already handled');
return cb(null, change);
} else {
change.rev = rev;
change.debug('updated revision (was ' + currentRev + ')');
if (change.checkpoint !== checkpoint) {
// previous revision is updated only across checkpoints
change.prev = currentRev;
change.debug('updated prev');
}
}
} else {
change.rev = null;
change.debug('updated revision (was ' + currentRev + ')');
if (change.checkpoint !== checkpoint) {
// previous revision is updated only across checkpoints
if (currentRev) {
change.prev = currentRev;
} else if (!change.prev) {
change.debug('ERROR - could not determine prev');
change.prev = Change.UNKNOWN;
}
change.debug('updated prev');
}
}
if (change.checkpoint != checkpoint) {
debug('update checkpoint to', checkpoint);
change.checkpoint = checkpoint;
}
if (change.prev === Change.UNKNOWN) {
// this occurs when a record of a change doesn't exist
// and its current revision is null (not found)
change.remove(cb);
} else {
change.save(cb);
}
}
};
/**
* Get a change's current revision based on current data.
* @callback {Function} callback
* @param {Error} err
* @param {String} rev The current revision
*/
Change.prototype.currentRevision = function(cb) {
cb = cb || utils.createPromiseCallback();
var model = this.getModelCtor();
var id = this.getModelId();
model.findById(id, function(err, inst) {
if (err) return cb(err);
if (inst) {
cb(null, Change.revisionForInst(inst));
} else {
cb(null, null);
}
});
return cb.promise;
};
/**
* Create a hash of the given `string` with the `options.hashAlgorithm`.
* **Default: `sha1`**
*
* @param {String} str The string to be hashed
* @return {String} The hashed string
*/
Change.hash = function(str) {
return crypto
.createHash(Change.settings.hashAlgorithm || 'sha1')
.update(str)
.digest('hex');
};
/**
* Get the revision string for the given object
* @param {Object} inst The data to get the revision string for
* @return {String} The revision string
*/
Change.revisionForInst = function(inst) {
assert(inst, 'Change.revisionForInst() requires an instance object.');
return this.hash(CJSON.stringify(inst));
};
/**
* Get a change's type. Returns one of:
*
* - `Change.UPDATE`
* - `Change.CREATE`
* - `Change.DELETE`
* - `Change.UNKNOWN`
*
* @return {String} the type of change
*/
Change.prototype.type = function() {
if (this.rev && this.prev) {
return Change.UPDATE;
}
if (this.rev && !this.prev) {
return Change.CREATE;
}
if (!this.rev && this.prev) {
return Change.DELETE;
}
return Change.UNKNOWN;
};
/**
* Compare two changes.
* @param {Change} change
* @return {Boolean}
*/
Change.prototype.equals = function(change) {
if (!change) return false;
var thisRev = this.rev || null;
var thatRev = change.rev || null;
return thisRev === thatRev;
};
/**
* Does this change conflict with the given change.
* @param {Change} change
* @return {Boolean}
*/
Change.prototype.conflictsWith = function(change) {
if (!change) return false;
if (this.equals(change)) return false;
if (Change.bothDeleted(this, change)) return false;
if (this.isBasedOn(change)) return false;
return true;
};
/**
* Are both changes deletes?
* @param {Change} a
* @param {Change} b
* @return {Boolean}
*/
Change.bothDeleted = function(a, b) {
return a.type() === Change.DELETE &&
b.type() === Change.DELETE;
};
/**
* Determine if the change is based on the given change.
* @param {Change} change
* @return {Boolean}
*/
Change.prototype.isBasedOn = function(change) {
return this.prev === change.rev;
};
/**
* Determine the differences for a given model since a given checkpoint.
*
* The callback will contain an error or `result`.
*
* **result**
*
* ```js
* {
* deltas: Array,
* conflicts: Array
* }
* ```
*
* **deltas**
*
* An array of changes that differ from `remoteChanges`.
*
* **conflicts**
*
* An array of changes that conflict with `remoteChanges`.
*
* @param {String} modelName
* @param {Number} since Compare changes after this checkpoint
* @param {Change[]} remoteChanges A set of changes to compare
* @callback {Function} callback
* @param {Error} err
* @param {Object} result See above.
*/
Change.diff = function(modelName, since, remoteChanges, callback) {
callback = callback || utils.createPromiseCallback();
if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) {
callback(null, {deltas: [], conflicts: []});
return callback.promise;
}
var remoteChangeIndex = {};
var modelIds = [];
remoteChanges.forEach(function(ch) {
modelIds.push(ch.modelId);
remoteChangeIndex[ch.modelId] = new Change(ch);
});
// normalize `since`
since = Number(since) || 0;
this.find({
where: {
modelName: modelName,
modelId: {inq: modelIds},
},
}, function(err, allLocalChanges) {
if (err) return callback(err);
var deltas = [];
var conflicts = [];
var localModelIds = [];
var localChanges = allLocalChanges.filter(function(c) {
return c.checkpoint >= since;
});
localChanges.forEach(function(localChange) {
localChange = new Change(localChange);
localModelIds.push(localChange.modelId);
var remoteChange = remoteChangeIndex[localChange.modelId];
if (remoteChange && !localChange.equals(remoteChange)) {
if (remoteChange.conflictsWith(localChange)) {
remoteChange.debug('remote conflict');
localChange.debug('local conflict');
conflicts.push(localChange);
} else {
remoteChange.debug('remote delta');
deltas.push(remoteChange);
}
}
});
modelIds.forEach(function(id) {
if (localModelIds.indexOf(id) !== -1) return;
var d = remoteChangeIndex[id];
var oldChange = allLocalChanges.filter(function(c) {
return c.modelId === id;
})[0];
if (oldChange) {
d.prev = oldChange.rev;
} else {
d.prev = null;
}
deltas.push(d);
});
callback(null, {
deltas: deltas,
conflicts: conflicts,
});
});
return callback.promise;
};
/**
* Correct all change list entries.
* @param {Function} cb
*/
Change.rectifyAll = function(cb) {
debug('rectify all');
var Change = this;
// this should be optimized
this.find(function(err, changes) {
if (err) return cb(err);
async.each(
changes,
function(c, next) { c.rectify(next); },
cb);
});
};
/**
* Get the checkpoint model.
* @return {Checkpoint}
*/
Change.getCheckpointModel = function() {
var checkpointModel = this.Checkpoint;
if (checkpointModel) return checkpointModel;
// FIXME(bajtos) This code creates multiple different models with the same
// model name, which is not a valid supported usage of juggler's API.
this.Checkpoint = checkpointModel = loopback.Checkpoint.extend('checkpoint');
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName +
' is not attached to a dataSource');
checkpointModel.attachTo(this.dataSource);
return checkpointModel;
};
Change.prototype.debug = function() {
if (debug.enabled) {
var args = Array.prototype.slice.call(arguments);
args[0] = args[0] + ' %s';
args.push(this.modelName);
debug.apply(this, args);
debug('\tid', this.id);
debug('\trev', this.rev);
debug('\tprev', this.prev);
debug('\tcheckpoint', this.checkpoint);
debug('\tmodelName', this.modelName);
debug('\tmodelId', this.modelId);
debug('\ttype', this.type());
}
};
/**
* Get the `Model` class for `change.modelName`.
* @return {Model}
*/
Change.prototype.getModelCtor = function() {
return this.constructor.settings.trackModel;
};
Change.prototype.getModelId = function() {
// TODO(ritch) get rid of the need to create an instance
var Model = this.getModelCtor();
var id = this.modelId;
var m = new Model();
m.setId(id);
return m.getId();
};
Change.prototype.getModel = function(callback) {
var Model = this.constructor.settings.trackModel;
var id = this.getModelId();
Model.findById(id, callback);
};
/**
* When two changes conflict a conflict is created.
*
* **Note**: call `conflict.fetch()` to get the `target` and `source` models.
*
* @param {*} modelId
* @param {PersistedModel} SourceModel
* @param {PersistedModel} TargetModel
* @property {ModelClass} source The source model instance
* @property {ModelClass} target The target model instance
* @class Change.Conflict
*/
function Conflict(modelId, SourceModel, TargetModel) {
this.SourceModel = SourceModel;
this.TargetModel = TargetModel;
this.SourceChange = SourceModel.getChangeModel();
this.TargetChange = TargetModel.getChangeModel();
this.modelId = modelId;
}
/**
* Fetch the conflicting models.
*
* @callback {Function} callback
* @param {Error} err
* @param {PersistedModel} source
* @param {PersistedModel} target
*/
Conflict.prototype.models = function(cb) {
var conflict = this;
var SourceModel = this.SourceModel;
var TargetModel = this.TargetModel;
var source, target;
async.parallel([
getSourceModel,
getTargetModel,
], done);
function getSourceModel(cb) {
SourceModel.findById(conflict.modelId, function(err, model) {
if (err) return cb(err);
source = model;
cb();
});
}
function getTargetModel(cb) {
TargetModel.findById(conflict.modelId, function(err, model) {
if (err) return cb(err);
target = model;
cb();
});
}
function done(err) {
if (err) return cb(err);
cb(null, source, target);
}
};
/**
* Get the conflicting changes.
*
* @callback {Function} callback
* @param {Error} err
* @param {Change} sourceChange
* @param {Change} targetChange
*/
Conflict.prototype.changes = function(cb) {
var conflict = this;
var sourceChange, targetChange;
async.parallel([
getSourceChange,
getTargetChange,
], done);
function getSourceChange(cb) {
var SourceModel = conflict.SourceModel;
SourceModel.findLastChange(conflict.modelId, function(err, change) {
if (err) return cb(err);
sourceChange = change;
cb();
});
}
function getTargetChange(cb) {
var TargetModel = conflict.TargetModel;
TargetModel.findLastChange(conflict.modelId, function(err, change) {
if (err) return cb(err);
targetChange = change;
cb();
});
}
function done(err) {
if (err) return cb(err);
cb(null, sourceChange, targetChange);
}
};
/**
* Resolve the conflict.
*
* Set the source change's previous revision to the current revision of the
* (conflicting) target change. Since the changes are no longer conflicting
* and appear as if the source change was based on the target, they will be
* replicated normally as part of the next replicate() call.
*
* This is effectively resolving the conflict using the source version.
*
* @callback {Function} callback
* @param {Error} err
*/
Conflict.prototype.resolve = function(cb) {
var conflict = this;
conflict.TargetModel.findLastChange(
this.modelId,
function(err, targetChange) {
if (err) return cb(err);
conflict.SourceModel.updateLastChange(
conflict.modelId,
{prev: targetChange.rev},
cb);
});
};
/**
* Resolve the conflict using the instance data in the source model.
*
* @callback {Function} callback
* @param {Error} err
*/
Conflict.prototype.resolveUsingSource = function(cb) {
this.resolve(function(err) {
// don't forward any cb arguments from resolve()
cb(err);
});
};
/**
* Resolve the conflict using the instance data in the target model.
*
* @callback {Function} callback
* @param {Error} err
*/
Conflict.prototype.resolveUsingTarget = function(cb) {
var conflict = this;
conflict.models(function(err, source, target) {
if (err) return done(err);
if (target === null) {
return conflict.SourceModel.deleteById(conflict.modelId, done);
}
var inst = new conflict.SourceModel(
target.toObject(),
{persisted: true});
inst.save(done);
});
function done(err) {
// don't forward any cb arguments from internal calls
cb(err);
}
};
/**
* Return a new Conflict instance with swapped Source and Target models.
*
* This is useful when resolving a conflict in one-way
* replication, where the source data must not be changed:
*
* ```js
* conflict.swapParties().resolveUsingTarget(cb);
* ```
*
* @returns {Conflict} A new Conflict instance.
*/
Conflict.prototype.swapParties = function() {
var Ctor = this.constructor;
return new Ctor(this.modelId, this.TargetModel, this.SourceModel);
};
/**
* Resolve the conflict using the supplied instance data.
*
* @param {Object} data The set of changes to apply on the model
* instance. Use `null` value to delete the source instance instead.
* @callback {Function} callback
* @param {Error} err
*/
Conflict.prototype.resolveManually = function(data, cb) {
var conflict = this;
if (!data) {
return conflict.SourceModel.deleteById(conflict.modelId, done);
}
conflict.models(function(err, source, target) {
if (err) return done(err);
var inst = source || new conflict.SourceModel(target);
inst.setAttributes(data);
inst.save(function(err) {
if (err) return done(err);
conflict.resolve(done);
});
});
function done(err) {
// don't forward any cb arguments from internal calls
cb(err);
}
};
/**
* Determine the conflict type.
*
* Possible results are
*
* - `Change.UPDATE`: Source and target models were updated.
* - `Change.DELETE`: Source and or target model was deleted.
* - `Change.UNKNOWN`: the conflict type is uknown or due to an error.
*
* @callback {Function} callback
* @param {Error} err
* @param {String} type The conflict type.
*/
Conflict.prototype.type = function(cb) {
var conflict = this;
this.changes(function(err, sourceChange, targetChange) {
if (err) return cb(err);
var sourceChangeType = sourceChange.type();
var targetChangeType = targetChange.type();
if (sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
return cb(null, Change.UPDATE);
}
if (sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
return cb(null, Change.DELETE);
}
return cb(null, Change.UNKNOWN);
});
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/**
* Module Dependencies.
*/
'use strict';
var assert = require('assert');
/**
* Checkpoint list entry.
*
* @property id {Number} the sequencial identifier of a checkpoint
* @property time {Number} the time when the checkpoint was created
* @property sourceId {String} the source identifier
*
* @class Checkpoint
* @inherits {PersistedModel}
*/
module.exports = function(Checkpoint) {
// Workaround for https://github.com/strongloop/loopback/issues/292
Checkpoint.definition.rawProperties.time.default =
Checkpoint.definition.properties.time.default = function() {
return new Date();
};
/**
* Get the current checkpoint id
* @callback {Function} callback
* @param {Error} err
* @param {Number} checkpoint The current checkpoint seq
*/
Checkpoint.current = function(cb) {
var Checkpoint = this;
Checkpoint._getSingleton(function(err, cp) {
cb(err, cp.seq);
});
};
Checkpoint._getSingleton = function(cb) {
var query = {limit: 1}; // match all instances, return only one
var initialData = {seq: 1};
this.findOrCreate(query, initialData, cb);
};
/**
* Increase the current checkpoint if it already exists otherwise initialize it
* @callback {Function} callback
* @param {Error} err
* @param {Object} checkpoint The current checkpoint
*/
Checkpoint.bumpLastSeq = function(cb) {
var Checkpoint = this;
Checkpoint._getSingleton(function(err, cp) {
if (err) return cb(err);
var originalSeq = cp.seq;
cp.seq++;
// Update the checkpoint but only if it was not changed under our hands
Checkpoint.updateAll({id: cp.id, seq: originalSeq}, {seq: cp.seq}, function(err, info) {
if (err) return cb(err);
// possible outcomes
// 1) seq was updated to seq+1 - exactly what we wanted!
// 2) somebody else already updated seq to seq+1 and our call was a no-op.
// That should be ok, checkpoints are time based, so we reuse the one created just now
// 3) seq was bumped more than once, so we will be using a value that is behind the latest seq.
// @bajtos is not entirely sure if this is ok, but since it wasn't handled by the current implementation either,
// he thinks we can keep it this way.
cb(null, cp);
});
});
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('../../lib/globalize');
/**
* Email model. Extends LoopBack base [Model](#model-new-model).
* @property {String} to Email addressee. Required.
* @property {String} from Email sender address. Required.
* @property {String} subject Email subject string. Required.
* @property {String} text Text body of email.
* @property {String} html HTML body of email.
*
* @class Email
* @inherits {Model}
*/
module.exports = function(Email) {
/**
* Send an email with the given `options`.
*
* Example Options:
*
* ```js
* {
* from: "Fred Foo <foo@blurdybloop.com>", // sender address
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
* subject: "Hello", // Subject line
* text: "Hello world", // plaintext body
* html: "<b>Hello world</b>" // html body
* }
* ```
*
* See https://github.com/andris9/Nodemailer for other supported options.
*
* @options {Object} options See below
* @prop {String} from Senders's email address
* @prop {String} to List of one or more recipient email addresses (comma-delimited)
* @prop {String} subject Subject line
* @prop {String} text Body text
* @prop {String} html Body HTML (optional)
* @param {Function} callback Called after the e-mail is sent or the sending failed
*/
Email.send = function() {
throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
};
/**
* A shortcut for Email.send(this).
*/
Email.prototype.send = function() {
throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('../../lib/globalize');
/**
* Data model for key-value databases.
*
* @class KeyValueModel
* @inherits {Model}
*/
module.exports = function(KeyValueModel) {
/**
* Return the value associated with a given key.
*
* @param {String} key Key to use when searching the database.
* @options {Object} options
* @callback {Function} callback
* @param {Error} err Error object.
* @param {Any} result Value associated with the given key.
* @promise
*
* @header KeyValueModel.get(key, cb)
*/
KeyValueModel.get = function(key, options, callback) {
throwNotAttached(this.modelName, 'get');
};
/**
* Persist a value and associate it with the given key.
*
* @param {String} key Key to associate with the given value.
* @param {Any} value Value to persist.
* @options {Number|Object} options Optional settings for the key-value
* pair. If a Number is provided, it is set as the TTL (time to live) in ms
* (milliseconds) for the key-value pair.
* @property {Number} ttl TTL for the key-value pair in ms.
* @callback {Function} callback
* @param {Error} err Error object.
* @promise
*
* @header KeyValueModel.set(key, value, cb)
*/
KeyValueModel.set = function(key, value, options, callback) {
throwNotAttached(this.modelName, 'set');
};
/**
* Set the TTL (time to live) in ms (milliseconds) for a given key. TTL is the
* remaining time before a key-value pair is discarded from the database.
*
* @param {String} key Key to use when searching the database.
* @param {Number} ttl TTL in ms to set for the key.
* @options {Object} options
* @callback {Function} callback
* @param {Error} err Error object.
* @promise
*
* @header KeyValueModel.expire(key, ttl, cb)
*/
KeyValueModel.expire = function(key, ttl, options, callback) {
throwNotAttached(this.modelName, 'expire');
};
/**
* Return the TTL (time to live) for a given key. TTL is the remaining time
* before a key-value pair is discarded from the database.
*
* @param {String} key Key to use when searching the database.
* @options {Object} options
* @callback {Function} callback
* @param {Error} error
* @param {Number} ttl Expiration time for the key-value pair. `undefined` if
* TTL was not initially set.
* @promise
*
* @header KeyValueModel.ttl(key, cb)
*/
KeyValueModel.ttl = function(key, options, callback) {
throwNotAttached(this.modelName, 'ttl');
};
/**
* Return all keys in the database.
*
* **WARNING**: This method is not suitable for large data sets as all
* key-values pairs are loaded into memory at once. For large data sets,
* use `iterateKeys()` instead.
*
* @param {Object} filter An optional filter object with the following
* @param {String} filter.match Glob string used to filter returned
* keys (i.e. `userid.*`). All connectors are required to support `*` and
* `?`, but may also support additional special characters specific to the
* database.
* @param {Object} options
* @callback {Function} callback
* @promise
*
* @header KeyValueModel.keys(filter, cb)
*/
KeyValueModel.keys = function(filter, options, callback) {
throwNotAttached(this.modelName, 'keys');
};
/**
* Asynchronously iterate all keys in the database. Similar to `.keys()` but
* instead allows for iteration over large data sets without having to load
* everything into memory at once.
*
* Callback example:
* ```js
* // Given a model named `Color` with two keys `red` and `blue`
* var iterator = Color.iterateKeys();
* it.next(function(err, key) {
* // key contains `red`
* it.next(function(err, key) {
* // key contains `blue`
* });
* });
* ```
*
* Promise example:
* ```js
* // Given a model named `Color` with two keys `red` and `blue`
* var iterator = Color.iterateKeys();
* Promise.resolve().then(function() {
* return it.next();
* })
* .then(function(key) {
* // key contains `red`
* return it.next();
* });
* .then(function(key) {
* // key contains `blue`
* });
* ```
*
* @param {Object} filter An optional filter object with the following
* @param {String} filter.match Glob string to use to filter returned
* keys (i.e. `userid.*`). All connectors are required to support `*` and
* `?`. They may also support additional special characters that are
* specific to the backing database.
* @param {Object} options
* @returns {AsyncIterator} An Object implementing `next(cb) -> Promise`
* function that can be used to iterate all keys.
*
* @header KeyValueModel.iterateKeys(filter)
*/
KeyValueModel.iterateKeys = function(filter, options) {
throwNotAttached(this.modelName, 'iterateKeys');
};
/*!
* Set up remoting metadata for this model.
*
* **Notes**:
* - The method is called automatically by `Model.extend` and/or
* `app.registry.createModel`
* - In general, base models use call this to ensure remote methods are
* inherited correctly, see bug at
* https://github.com/strongloop/loopback/issues/2350
*/
KeyValueModel.setup = function() {
KeyValueModel.base.setup.apply(this, arguments);
this.remoteMethod('get', {
accepts: {
arg: 'key', type: 'string', required: true,
http: {source: 'path'},
},
returns: {arg: 'value', type: 'any', root: true},
http: {path: '/:key', verb: 'get'},
rest: {after: convertNullToNotFoundError},
});
this.remoteMethod('set', {
accepts: [
{arg: 'key', type: 'string', required: true,
http: {source: 'path'}},
{arg: 'value', type: 'any', required: true,
http: {source: 'body'}},
{arg: 'ttl', type: 'number',
http: {source: 'query'},
description: 'time to live in milliseconds'},
],
http: {path: '/:key', verb: 'put'},
});
this.remoteMethod('expire', {
accepts: [
{arg: 'key', type: 'string', required: true,
http: {source: 'path'}},
{arg: 'ttl', type: 'number', required: true,
http: {source: 'form'}},
],
http: {path: '/:key/expire', verb: 'put'},
});
this.remoteMethod('ttl', {
accepts: {
arg: 'key', type: 'string', required: true,
http: {source: 'path'},
},
returns: {arg: 'value', type: 'any', root: true},
http: {path: '/:key/ttl', verb: 'get'},
});
this.remoteMethod('keys', {
accepts: {
arg: 'filter', type: 'object', required: false,
http: {source: 'query'},
},
returns: {arg: 'keys', type: ['string'], root: true},
http: {path: '/keys', verb: 'get'},
});
};
};
function throwNotAttached(modelName, methodName) {
throw new Error(g.f(
'Cannot call %s.%s(). ' +
'The %s method has not been setup. ' +
'The {{KeyValueModel}} has not been correctly attached ' +
'to a {{DataSource}}!',
modelName, methodName, methodName));
}
function convertNullToNotFoundError(ctx, cb) {
if (ctx.result !== null) return cb();
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
var msg = g.f('Unknown "%s" {{key}} "%s".', modelName, id);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'KEY_NOT_FOUND';
cb(error);
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2015. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils');
/**
* The `RoleMapping` model extends from the built in `loopback.Model` type.
*
* @property {String} id Generated ID.
* @property {String} name Name of the role.
* @property {String} Description Text description.
*
* @class RoleMapping
* @inherits {PersistedModel}
*/
module.exports = function(RoleMapping) {
// Principal types
RoleMapping.USER = 'USER';
RoleMapping.APP = RoleMapping.APPLICATION = 'APP';
RoleMapping.ROLE = 'ROLE';
RoleMapping.resolveRelatedModels = function() {
if (!this.userModel) {
var reg = this.registry;
this.roleModel = reg.getModelByType('Role');
this.userModel = reg.getModelByType('User');
this.applicationModel = reg.getModelByType('Application');
}
};
/**
* Get the application principal
* @callback {Function} callback
* @param {Error} err
* @param {Application} application
*/
RoleMapping.prototype.application = function(callback) {
callback = callback || utils.createPromiseCallback();
this.constructor.resolveRelatedModels();
if (this.principalType === RoleMapping.APPLICATION) {
var applicationModel = this.constructor.applicationModel;
applicationModel.findById(this.principalId, callback);
} else {
process.nextTick(function() {
callback(null, null);
});
}
return callback.promise;
};
/**
* Get the user principal
* @callback {Function} callback
* @param {Error} err
* @param {User} user
*/
RoleMapping.prototype.user = function(callback) {
callback = callback || utils.createPromiseCallback();
this.constructor.resolveRelatedModels();
var userModel;
if (this.principalType === RoleMapping.USER) {
userModel = this.constructor.userModel;
userModel.findById(this.principalId, callback);
return callback.promise;
}
// try resolving a user model that matches principalType
userModel = this.constructor.registry.findModel(this.principalType);
if (userModel) {
userModel.findById(this.principalId, callback);
} else {
process.nextTick(function() {
callback(null, null);
});
}
return callback.promise;
};
/**
* Get the child role principal
* @callback {Function} callback
* @param {Error} err
* @param {User} childUser
*/
RoleMapping.prototype.childRole = function(callback) {
callback = callback || utils.createPromiseCallback();
this.constructor.resolveRelatedModels();
if (this.principalType === RoleMapping.ROLE) {
var roleModel = this.constructor.roleModel;
roleModel.findById(this.principalId, callback);
} else {
process.nextTick(function() {
callback(null, null);
});
}
return callback.promise;
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 1 4 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var loopback = require('../../lib/loopback');
var debug = require('debug')('loopback:security:role');
var assert = require('assert');
var async = require('async');
var utils = require('../../lib/utils');
var ctx = require('../../lib/access-context');
var AccessContext = ctx.AccessContext;
var Principal = ctx.Principal;
var RoleMapping = loopback.RoleMapping;
assert(RoleMapping, 'RoleMapping model must be defined before Role model');
/**
* The Role model
* @class Role
* @header Role object
*/
module.exports = function(Role) {
Role.resolveRelatedModels = function() {
if (!this.userModel) {
var reg = this.registry;
this.roleMappingModel = reg.getModelByType('RoleMapping');
this.userModel = reg.getModelByType('User');
this.applicationModel = reg.getModelByType('Application');
}
};
// Set up the connection to users/applications/roles once the model
Role.once('dataSourceAttached', function(roleModel) {
['users', 'applications', 'roles'].forEach(function(rel) {
/**
* Fetch all users assigned to this role
* @function Role.prototype#users
* @param {object} [query] query object passed to model find call
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Array} list The list of users.
* @promise
*/
/**
* Fetch all applications assigned to this role
* @function Role.prototype#applications
* @param {object} [query] query object passed to model find call
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Array} list The list of applications.
* @promise
*/
/**
* Fetch all roles assigned to this role
* @function Role.prototype#roles
* @param {object} [query] query object passed to model find call
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Array} list The list of roles.
* @promise
*/
Role.prototype[rel] = function(query, callback) {
if (!callback) {
if (typeof query === 'function') {
callback = query;
query = {};
} else {
callback = utils.createPromiseCallback();
}
}
query = query || {};
query.where = query.where || {};
roleModel.resolveRelatedModels();
var relsToModels = {
users: roleModel.userModel,
applications: roleModel.applicationModel,
roles: roleModel,
};
var ACL = loopback.ACL;
var relsToTypes = {
users: ACL.USER,
applications: ACL.APP,
roles: ACL.ROLE,
};
var principalModel = relsToModels[rel];
var principalType = relsToTypes[rel];
// redefine user model and user type if user principalType is custom (available and not "USER")
var isCustomUserPrincipalType = rel === 'users' &&
query.where.principalType &&
query.where.principalType !== RoleMapping.USER;
if (isCustomUserPrincipalType) {
var registry = this.constructor.registry;
principalModel = registry.findModel(query.where.principalType);
principalType = query.where.principalType;
}
// make sure we don't keep principalType in userModel query
delete query.where.principalType;
if (principalModel) {
listByPrincipalType(this, principalModel, principalType, query, callback);
} else {
process.nextTick(function() {
callback(null, []);
});
}
return callback.promise;
};
});
/**
* Fetch all models assigned to this role
* @private
* @param {object} Context role context
* @param {*} model model type to fetch
* @param {String} [principalType] principalType used in the rolemapping for model
* @param {object} [query] query object passed to model find call
* @param {Function} [callback] callback function called with `(err, models)` arguments.
*/
function listByPrincipalType(context, model, principalType, query, callback) {
if (callback === undefined && typeof query === 'function') {
callback = query;
query = {};
}
query = query || {};
roleModel.roleMappingModel.find({
where: {roleId: context.id, principalType: principalType},
}, function(err, mappings) {
var ids;
if (err) {
return callback(err);
}
ids = mappings.map(function(m) {
return m.principalId;
});
query.where = query.where || {};
query.where.id = {inq: ids};
model.find(query, function(err, models) {
callback(err, models);
});
});
}
});
// Special roles
Role.OWNER = '$owner'; // owner of the object
Role.RELATED = '$related'; // any User with a relationship to the object
Role.AUTHENTICATED = '$authenticated'; // authenticated user
Role.UNAUTHENTICATED = '$unauthenticated'; // authenticated user
Role.EVERYONE = '$everyone'; // everyone
/**
* Add custom handler for roles.
* @param {String} role Name of role.
* @param {Function} resolver Function that determines
* if a principal is in the specified role.
* Should provide a callback or return a promise.
*/
Role.registerResolver = function(role, resolver) {
if (!Role.resolvers) {
Role.resolvers = {};
}
Role.resolvers[role] = resolver;
};
Role.registerResolver(Role.OWNER, function(role, context, callback) {
if (!context || !context.model || !context.modelId) {
process.nextTick(function() {
if (callback) callback(null, false);
});
return;
}
var modelClass = context.model;
var modelId = context.modelId;
var user = context.getUser();
var userId = user && user.id;
var principalType = user && user.principalType;
var opts = {accessToken: context.accessToken};
Role.isOwner(modelClass, modelId, userId, principalType, opts, callback);
});
function isUserClass(modelClass) {
if (!modelClass) return false;
var User = modelClass.modelBuilder.models.User;
if (!User) return false;
return modelClass == User || modelClass.prototype instanceof User;
}
/*!
* Check if two user IDs matches
* @param {*} id1
* @param {*} id2
* @returns {boolean}
*/
function matches(id1, id2) {
if (id1 === undefined || id1 === null || id1 === '' ||
id2 === undefined || id2 === null || id2 === '') {
return false;
}
// The id can be a MongoDB ObjectID
return id1 === id2 || id1.toString() === id2.toString();
}
/**
* Check if a given user ID is the owner the model instance.
* @param {Function} modelClass The model class
* @param {*} modelId The model ID
* @param {*} userId The user ID
* @param {String} principalType The user principalType (optional)
* @options {Object} options
* @property {accessToken} The access token used to authorize the current user.
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Boolean} isOwner True if the user is an owner.
* @promise
*/
Role.isOwner = function isOwner(modelClass, modelId, userId, principalType, options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = {};
} else if (!callback && typeof principalType === 'function') {
callback = principalType;
principalType = undefined;
options = {};
}
principalType = principalType || Principal.USER;
assert(modelClass, 'Model class is required');
if (!callback) callback = utils.createPromiseCallback();
debug('isOwner(): %s %s userId: %s principalType: %s',
modelClass && modelClass.modelName, modelId, userId, principalType);
// Return false if userId is missing
if (!userId) {
process.nextTick(function() {
callback(null, false);
});
return callback.promise;
}
// Is the modelClass User or a subclass of User?
if (isUserClass(modelClass)) {
var userModelName = modelClass.modelName;
// matching ids is enough if principalType is USER or matches given user model name
if (principalType === Principal.USER || principalType === userModelName) {
process.nextTick(function() {
callback(null, matches(modelId, userId));
});
}
return callback.promise;
}
modelClass.findById(modelId, options, function(err, inst) {
if (err || !inst) {
debug('Model not found for id %j', modelId);
return callback(err, false);
}
debug('Model found: %j', inst);
// Historically, for principalType USER, we were resolving isOwner()
// as true if the model has "userId" or "owner" property matching
// id of the current user (principalId), even though there was no
// belongsTo relation set up.
var ownerId = inst.userId || inst.owner;
if (principalType === Principal.USER && ownerId && 'function' !== typeof ownerId) {
return callback(null, matches(ownerId, userId));
}
// Try to follow belongsTo
for (var r in modelClass.relations) {
var rel = modelClass.relations[r];
// relation should be belongsTo and target a User based class
var belongsToUser = rel.type === 'belongsTo' && isUserClass(rel.modelTo);
if (!belongsToUser) {
continue;
}
// checking related user
var userModelName = rel.modelTo.modelName;
if (principalType === Principal.USER || principalType === userModelName) {
debug('Checking relation %s to %s: %j', r, userModelName, rel);
inst[r](processRelatedUser);
return;
}
}
debug('No matching belongsTo relation found for model %j - user %j principalType %j',
modelId, userId, principalType);
callback(null, false);
function processRelatedUser(err, user) {
if (!err && user) {
debug('User found: %j', user.id);
callback(null, matches(user.id, userId));
} else {
callback(err, false);
}
}
});
return callback.promise;
};
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
if (!context) {
process.nextTick(function() {
if (callback) callback(null, false);
});
return;
}
Role.isAuthenticated(context, callback);
});
/**
* Check if the user ID is authenticated
* @param {Object} context The security context.
*
* @callback {Function} callback Callback function.
* @param {Error} err Error object.
* @param {Boolean} isAuthenticated True if the user is authenticated.
* @promise
*/
Role.isAuthenticated = function isAuthenticated(context, callback) {
if (!callback) callback = utils.createPromiseCallback();
process.nextTick(function() {
if (callback) callback(null, context.isAuthenticated());
});
return callback.promise;
};
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
process.nextTick(function() {
if (callback) callback(null, !context || !context.isAuthenticated());
});
});
Role.registerResolver(Role.EVERYONE, function(role, context, callback) {
process.nextTick(function() {
if (callback) callback(null, true); // Always true
});
});
/**
* Check if a given principal is in the specified role.
*
* @param {String} role The role name.
* @param {Object} context The context object.
*
* @callback {Function} callback Callback function.
* @param {Error} err Error object.
* @param {Boolean} isInRole True if the principal is in the specified role.
* @promise
*/
Role.isInRole = function(role, context, callback) {
context.registry = this.registry;
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
if (!callback) {
callback = utils.createPromiseCallback();
// historically, isInRole is returning the Role instance instead of true
// we are preserving that behaviour for callback-based invocation,
// but fixing it when invoked in Promise mode
callback.promise = callback.promise.then(function(isInRole) {
return !!isInRole;
});
}
this.resolveRelatedModels();
debug('isInRole(): %s', role);
context.debug();
var resolver = Role.resolvers[role];
if (resolver) {
debug('Custom resolver found for role %s', role);
var promise = resolver(role, context, callback);
if (promise && typeof promise.then === 'function') {
promise.then(
function(result) { callback(null, result); },
callback
);
}
return callback.promise;
}
if (context.principals.length === 0) {
debug('isInRole() returns: false');
process.nextTick(function() {
if (callback) callback(null, false);
});
return callback.promise;
}
var inRole = context.principals.some(function(p) {
var principalType = p.type || undefined;
var principalId = p.id || undefined;
// Check if it's the same role
return principalType === RoleMapping.ROLE && principalId === role;
});
if (inRole) {
debug('isInRole() returns: %j', inRole);
process.nextTick(function() {
if (callback) callback(null, true);
});
return callback.promise;
}
var roleMappingModel = this.roleMappingModel;
this.findOne({where: {name: role}}, function(err, result) {
if (err) {
if (callback) callback(err);
return;
}
if (!result) {
if (callback) callback(null, false);
return;
}
debug('Role found: %j', result);
// Iterate through the list of principals
async.some(context.principals, function(p, done) {
var principalType = p.type || undefined;
var principalId = p.id || undefined;
var roleId = result.id.toString();
var principalIdIsString = typeof principalId === 'string';
if (principalId !== null && principalId !== undefined && !principalIdIsString) {
principalId = principalId.toString();
}
if (principalType && principalId) {
roleMappingModel.findOne({where: {roleId: roleId,
principalType: principalType, principalId: principalId}},
function(err, result) {
debug('Role mapping found: %j', result);
done(!err && result); // The only arg is the result
});
} else {
process.nextTick(function() {
done(false);
});
}
}, function(inRole) {
debug('isInRole() returns: %j', inRole);
if (callback) callback(null, inRole);
});
});
return callback.promise;
};
/**
* List roles for a given principal.
* @param {Object} context The security context.
*
* @callback {Function} callback Callback function.
* @param {Error} err Error object.
* @param {String[]} roles An array of role IDs
* @promise
*/
Role.getRoles = function(context, options, callback) {
if (!callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else {
callback = utils.createPromiseCallback();
}
}
if (!options) options = {};
context.registry = this.registry;
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
var roles = [];
this.resolveRelatedModels();
var addRole = function(role) {
if (role && roles.indexOf(role) === -1) {
roles.push(role);
}
};
var self = this;
// Check against the smart roles
var inRoleTasks = [];
Object.keys(Role.resolvers).forEach(function(role) {
inRoleTasks.push(function(done) {
self.isInRole(role, context, function(err, inRole) {
if (debug.enabled) {
debug('In role %j: %j', role, inRole);
}
if (!err && inRole) {
addRole(role);
done();
} else {
done(err, null);
}
});
});
});
var roleMappingModel = this.roleMappingModel;
context.principals.forEach(function(p) {
// Check against the role mappings
var principalType = p.type || undefined;
var principalId = p.id == null ? undefined : p.id;
if (typeof principalId !== 'string' && principalId != null) {
principalId = principalId.toString();
}
// Add the role itself
if (principalType === RoleMapping.ROLE && principalId) {
addRole(principalId);
}
if (principalType && principalId) {
// Please find() treat undefined matches all values
inRoleTasks.push(function(done) {
var filter = {where: {principalType: principalType, principalId: principalId}};
if (options.returnOnlyRoleNames === true) {
filter.include = ['role'];
}
roleMappingModel.find(filter, function(err, mappings) {
debug('Role mappings found: %s %j', err, mappings);
if (err) {
if (done) done(err);
return;
}
mappings.forEach(function(m) {
var role;
if (options.returnOnlyRoleNames === true) {
role = m.toJSON().role.name;
} else {
role = m.roleId;
}
addRole(role);
});
if (done) done();
});
});
}
});
async.parallel(inRoleTasks, function(err, results) {
debug('getRoles() returns: %j %j', err, roles);
if (callback) callback(err, roles);
});
return callback.promise;
};
Role.validatesUniquenessOf('name', {message: 'already exists'});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var loopback = require('../../lib/loopback');
/**
* Resource owner grants/delegates permissions to client applications
*
* For a protected resource, does the client application have the authorization
* from the resource owner (user or system)?
*
* Scope has many resource access entries
*
* @class Scope
*/
module.exports = function(Scope) {
Scope.resolveRelatedModels = function() {
if (!this.aclModel) {
var reg = this.registry;
this.aclModel = reg.getModelByType(loopback.ACL);
}
};
/**
* Check if the given scope is allowed to access the model/property
* @param {String} scope The scope name
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
* @callback {Function} callback
* @param {String|Error} err The error object
* @param {AccessRequest} result The access permission
*/
Scope.checkPermission = function(scope, model, property, accessType, callback) {
this.resolveRelatedModels();
var aclModel = this.aclModel;
assert(aclModel,
'ACL model must be defined before Scope.checkPermission is called');
this.findOne({where: {name: scope}}, function(err, scope) {
if (err) {
if (callback) callback(err);
} else {
aclModel.checkPermission(
aclModel.SCOPE, scope.id, model, property, accessType, callback);
}
});
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module Dependencies.
*/
'use strict';
var g = require('../../lib/globalize');
var isEmail = require('isemail');
var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils');
var path = require('path');
var qs = require('querystring');
var SALT_WORK_FACTOR = 10;
var crypto = require('crypto');
var MAX_PASSWORD_LENGTH = 72;
var bcrypt;
try {
// Try the native module first
bcrypt = require('bcrypt');
// Browserify returns an empty object
if (bcrypt && typeof bcrypt.compare !== 'function') {
bcrypt = require('bcryptjs');
}
} catch (err) {
// Fall back to pure JS impl
bcrypt = require('bcryptjs');
}
var DEFAULT_TTL = 1209600; // 2 weeks in seconds
var DEFAULT_RESET_PW_TTL = 15 * 60; // 15 mins in seconds
var DEFAULT_MAX_TTL = 31556926; // 1 year in seconds
var assert = require('assert');
var debug = require('debug')('loopback:user');
/**
* Built-in User model.
* Extends LoopBack [PersistedModel](#persistedmodel-new-persistedmodel).
*
* Default `User` ACLs.
*
* - DENY EVERYONE `*`
* - ALLOW EVERYONE `create`
* - ALLOW OWNER `deleteById`
* - ALLOW EVERYONE `login`
* - ALLOW EVERYONE `logout`
* - ALLOW OWNER `findById`
* - ALLOW OWNER `updateAttributes`
*
* @property {String} username Must be unique.
* @property {String} password Hidden from remote clients.
* @property {String} email Must be valid email.
* @property {Boolean} emailVerified Set when a user's email has been verified via `confirm()`.
* @property {String} verificationToken Set when `verify()` is called.
* @property {String} realm The namespace the user belongs to. See [Partitioning users with realms](http://loopback.io/doc/en/lb2/Partitioning-users-with-realms.html) for details.
* @property {Object} settings Extends the `Model.settings` object.
* @property {Boolean} settings.emailVerificationRequired Require the email verification
* process before allowing a login.
* @property {Number} settings.ttl Default time to live (in seconds) for the `AccessToken` created by `User.login() / user.createAccessToken()`.
* Default is `1209600` (2 weeks)
* @property {Number} settings.maxTTL The max value a user can request a token to be alive / valid for.
* Default is `31556926` (1 year)
* @property {Boolean} settings.realmRequired Require a realm when logging in a user.
* @property {String} settings.realmDelimiter When set a realm is required.
* @property {Number} settings.resetPasswordTokenTTL Time to live for password reset `AccessToken`. Default is `900` (15 minutes).
* @property {Number} settings.saltWorkFactor The `bcrypt` salt work factor. Default is `10`.
* @property {Boolean} settings.caseSensitiveEmail Enable case sensitive email.
*
* @class User
* @inherits {PersistedModel}
*/
module.exports = function(User) {
/**
* Create access token for the logged in user. This method can be overridden to
* customize how access tokens are generated
*
* @param {Number} ttl The requested ttl
* @param {Object} [options] The options for access token, such as scope, appId
* @callback {Function} cb The callback function
* @param {String|Error} err The error string or object
* @param {AccessToken} token The generated access token object
* @promise
*/
User.prototype.createAccessToken = function(ttl, options, cb) {
if (cb === undefined && typeof options === 'function') {
// createAccessToken(ttl, cb)
cb = options;
options = undefined;
}
cb = cb || utils.createPromiseCallback();
if (typeof ttl === 'object' && !options) {
// createAccessToken(options, cb)
options = ttl;
ttl = options.ttl;
}
options = options || {};
var userModel = this.constructor;
ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL);
this.accessTokens.create({
ttl: ttl,
}, cb);
return cb.promise;
};
function splitPrincipal(name, realmDelimiter) {
var parts = [null, name];
if (!realmDelimiter) {
return parts;
}
var index = name.indexOf(realmDelimiter);
if (index !== -1) {
parts[0] = name.substring(0, index);
parts[1] = name.substring(index + realmDelimiter.length);
}
return parts;
}
/**
* Normalize the credentials
* @param {Object} credentials The credential object
* @param {Boolean} realmRequired
* @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed
* @returns {Object} The normalized credential object
*/
User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) {
var query = {};
credentials = credentials || {};
if (!realmRequired) {
if (credentials.email) {
query.email = credentials.email;
} else if (credentials.username) {
query.username = credentials.username;
}
} else {
if (credentials.realm) {
query.realm = credentials.realm;
}
var parts;
if (credentials.email) {
parts = splitPrincipal(credentials.email, realmDelimiter);
query.email = parts[1];
if (parts[0]) {
query.realm = parts[0];
}
} else if (credentials.username) {
parts = splitPrincipal(credentials.username, realmDelimiter);
query.username = parts[1];
if (parts[0]) {
query.realm = parts[0];
}
}
}
return query;
};
/**
* Login a user by with the given `credentials`.
*
* ```js
* User.login({username: 'foo', password: 'bar'}, function (err, token) {
* console.log(token.id);
* });
* ```
*
* @param {Object} credentials username/password or email/password
* @param {String[]|String} [include] Optionally set it to "user" to include
* the user info
* @callback {Function} callback Callback function
* @param {Error} err Error object
* @param {AccessToken} token Access token if login is successful
* @promise
*/
User.login = function(credentials, include, fn) {
var self = this;
if (typeof include === 'function') {
fn = include;
include = undefined;
}
fn = fn || utils.createPromiseCallback();
include = (include || '');
if (Array.isArray(include)) {
include = include.map(function(val) {
return val.toLowerCase();
});
} else {
include = include.toLowerCase();
}
var realmDelimiter;
// Check if realm is required
var realmRequired = !!(self.settings.realmRequired ||
self.settings.realmDelimiter);
if (realmRequired) {
realmDelimiter = self.settings.realmDelimiter;
}
var query = self.normalizeCredentials(credentials, realmRequired,
realmDelimiter);
if (realmRequired && !query.realm) {
var err1 = new Error(g.f('{{realm}} is required'));
err1.statusCode = 400;
err1.code = 'REALM_REQUIRED';
fn(err1);
return fn.promise;
}
if (!query.email && !query.username) {
var err2 = new Error(g.f('{{username}} or {{email}} is required'));
err2.statusCode = 400;
err2.code = 'USERNAME_EMAIL_REQUIRED';
fn(err2);
return fn.promise;
}
self.findOne({where: query}, function(err, user) {
var defaultError = new Error(g.f('login failed'));
defaultError.statusCode = 401;
defaultError.code = 'LOGIN_FAILED';
function tokenHandler(err, token) {
if (err) return fn(err);
if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') {
// NOTE(bajtos) We can't set token.user here:
// 1. token.user already exists, it's a function injected by
// "AccessToken belongsTo User" relation
// 2. ModelBaseClass.toJSON() ignores own properties, thus
// the value won't be included in the HTTP response
// See also loopback#161 and loopback#162
token.__data.user = user;
}
fn(err, token);
}
if (err) {
debug('An error is reported from User.findOne: %j', err);
fn(defaultError);
} else if (user) {
user.hasPassword(credentials.password, function(err, isMatch) {
if (err) {
debug('An error is reported from User.hasPassword: %j', err);
fn(defaultError);
} else if (isMatch) {
if (self.settings.emailVerificationRequired && !user.emailVerified) {
// Fail to log in if email verification is not done yet
debug('User email has not been verified');
err = new Error(g.f('login failed as the email has not been verified'));
err.statusCode = 401;
err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED';
fn(err);
} else {
if (user.createAccessToken.length === 2) {
user.createAccessToken(credentials.ttl, tokenHandler);
} else {
user.createAccessToken(credentials.ttl, credentials, tokenHandler);
}
}
} else {
debug('The password is invalid for user %s', query.email || query.username);
fn(defaultError);
}
});
} else {
debug('No matching record is found for user %s', query.email || query.username);
fn(defaultError);
}
});
return fn.promise;
};
/**
* Logout a user with the given accessToken id.
*
* ```js
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
* console.log(err || 'Logged out');
* });
* ```
*
* @param {String} accessTokenID
* @callback {Function} callback
* @param {Error} err
* @promise
*/
User.logout = function(tokenId, fn) {
fn = fn || utils.createPromiseCallback();
var err;
if (!tokenId) {
err = new Error(g.f('{{accessToken}} is required to logout'));
err.status = 401;
process.nextTick(fn, err);
return fn.promise;
}
this.relations.accessTokens.modelTo.destroyById(tokenId, function(err, info) {
if (err) {
fn(err);
} else if ('count' in info && info.count === 0) {
err = new Error(g.f('Could not find {{accessToken}}'));
err.status = 401;
fn(err);
} else {
fn();
}
});
return fn.promise;
};
User.observe('before delete', function(ctx, next) {
var AccessToken = ctx.Model.relations.accessTokens.modelTo;
var pkName = ctx.Model.definition.idName() || 'id';
ctx.Model.find({where: ctx.where, fields: [pkName]}, function(err, list) {
if (err) return next(err);
var ids = list.map(function(u) { return u[pkName]; });
ctx.where = {};
ctx.where[pkName] = {inq: ids};
AccessToken.destroyAll({userId: {inq: ids}}, next);
});
});
/**
* Compare the given `password` with the users hashed password.
*
* @param {String} password The plain text password
* @callback {Function} callback Callback function
* @param {Error} err Error object
* @param {Boolean} isMatch Returns true if the given `password` matches record
* @promise
*/
User.prototype.hasPassword = function(plain, fn) {
fn = fn || utils.createPromiseCallback();
if (this.password && plain) {
bcrypt.compare(plain, this.password, function(err, isMatch) {
if (err) return fn(err);
fn(null, isMatch);
});
} else {
fn(null, false);
}
return fn.promise;
};
/**
* Change this user's password.
*
* @param {*} userId Id of the user changing the password
* @param {string} oldPassword Current password, required in order
* to strongly verify the identity of the requesting user
* @param {string} newPassword The new password to use.
* @param {object} [options]
* @callback {Function} callback
* @param {Error} err Error object
* @promise
*/
User.changePassword = function(userId, oldPassword, newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') {
cb = options;
options = undefined;
}
cb = cb || utils.createPromiseCallback();
// Make sure to use the constructor of the (sub)class
// where the method is invoked from (`this` instead of `User`)
this.findById(userId, options, (err, inst) => {
if (err) return cb(err);
if (!inst) {
const err = new Error(`User ${userId} not found`);
Object.assign(err, {
code: 'USER_NOT_FOUND',
statusCode: 401,
});
return cb(err);
}
inst.changePassword(oldPassword, newPassword, options, cb);
});
return cb.promise;
};
/**
* Change this user's password (prototype/instance version).
*
* @param {string} oldPassword Current password, required in order
* to strongly verify the identity of the requesting user
* @param {string} newPassword The new password to use.
* @param {object} [options]
* @callback {Function} callback
* @param {Error} err Error object
* @promise
*/
User.prototype.changePassword = function(oldPassword, newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') {
cb = options;
options = undefined;
}
cb = cb || utils.createPromiseCallback();
this.hasPassword(oldPassword, (err, isMatch) => {
if (err) return cb(err);
if (!isMatch) {
const err = new Error('Invalid current password');
Object.assign(err, {
code: 'INVALID_PASSWORD',
statusCode: 400,
});
return cb(err);
}
try {
User.validatePassword(newPassword);
} catch (err) {
return cb(err);
}
const delta = {password: newPassword};
this.patchAttributes(delta, options, (err, updated) => cb(err));
});
return cb.promise;
};
/**
* Verify a user's identity by sending them a confirmation email.
*
* ```js
* var options = {
* type: 'email',
* to: user.email,
* template: 'verify.ejs',
* redirect: '/',
* tokenGenerator: function (user, cb) { cb("random-token"); }
* };
*
* user.verify(options, next);
* ```
*
* @options {Object} options
* @property {String} type Must be 'email'.
* @property {String} to Email address to which verification email is sent.
* @property {String} from Sender email addresss, for example
* `'noreply@myapp.com'`.
* @property {String} subject Subject line text.
* @property {String} text Text of email.
* @property {String} template Name of template that displays verification
* page, for example, `'verify.ejs'.
* @property {Function} templateFn A function generating the email HTML body
* from `verify()` options object and generated attributes like `options.verifyHref`.
* It must accept the option object and a callback function with `(err, html)`
* as parameters
* @property {String} redirect Page to which user will be redirected after
* they verify their email, for example `'/'` for root URI.
* @property {Function} generateVerificationToken A function to be used to
* generate the verification token. It must accept the user object and a
* callback function. This function should NOT add the token to the user
* object, instead simply execute the callback with the token! User saving
* and email sending will be handled in the `verify()` method.
* @callback {Function} fn Callback function.
* @param {Error} err Error object.
* @param {Object} object Contains email, token, uid.
* @promise
*/
User.prototype.verify = function(options, fn) {
fn = fn || utils.createPromiseCallback();
var user = this;
var userModel = this.constructor;
var registry = userModel.registry;
var pkName = userModel.definition.idName() || 'id';
assert(typeof options === 'object', 'options required when calling user.verify()');
assert(options.type, 'You must supply a verification type (options.type)');
assert(options.type === 'email', 'Unsupported verification type');
assert(options.to || this.email,
'Must include options.to when calling user.verify() ' +
'or the user must have an email property');
assert(options.from, 'Must include options.from when calling user.verify()');
options.redirect = options.redirect || '/';
var defaultTemplate = path.join(__dirname, '..', '..', 'templates', 'verify.ejs');
options.template = path.resolve(options.template || defaultTemplate);
options.user = this;
options.protocol = options.protocol || 'http';
var app = userModel.app;
options.host = options.host || (app && app.get('host')) || 'localhost';
options.port = options.port || (app && app.get('port')) || 3000;
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
var displayPort = (
(options.protocol === 'http' && options.port == '80') ||
(options.protocol === 'https' && options.port == '443')
) ? '' : ':' + options.port;
var urlPath = joinUrlPath(
options.restApiRoot,
userModel.http.path,
userModel.sharedClass.findMethodByName('confirm').http.path
);
options.verifyHref = options.verifyHref ||
options.protocol +
'://' +
options.host +
displayPort +
urlPath +
'?' + qs.stringify({
uid: '' + options.user[pkName],
redirect: options.redirect,
});
options.templateFn = options.templateFn || createVerificationEmailBody;
// Email model
var Email =
options.mailer || this.constructor.email || registry.getModelByType(loopback.Email);
// Set a default token generation function if one is not provided
var tokenGenerator = options.generateVerificationToken || User.generateVerificationToken;
assert(typeof tokenGenerator === 'function', 'generateVerificationToken must be a function');
tokenGenerator(user, function(err, token) {
if (err) { return fn(err); }
user.verificationToken = token;
user.save(function(err) {
if (err) {
fn(err);
} else {
sendEmail(user);
}
});
});
// TODO - support more verification types
function sendEmail(user) {
options.verifyHref += '&token=' + user.verificationToken;
options.verificationToken = user.verificationToken;
options.text = options.text || g.f('Please verify your email by opening ' +
'this link in a web browser:\n\t%s', options.verifyHref);
options.text = options.text.replace(/\{href\}/g, options.verifyHref);
options.to = options.to || user.email;
options.subject = options.subject || g.f('Thanks for Registering');
options.headers = options.headers || {};
options.templateFn(options, function(err, html) {
if (err) {
fn(err);
} else {
setHtmlContentAndSend(html);
}
});
function setHtmlContentAndSend(html) {
options.html = html;
// Remove options.template to prevent rejection by certain
// nodemailer transport plugins.
delete options.template;
Email.send(options, function(err, email) {
if (err) {
fn(err);
} else {
fn(null, {email: email, token: user.verificationToken, uid: user[pkName]});
}
});
}
}
return fn.promise;
};
function createVerificationEmailBody(options, cb) {
var template = loopback.template(options.template);
var body = template(options);
cb(null, body);
}
/**
* A default verification token generator which accepts the user the token is
* being generated for and a callback function to indicate completion.
* This one uses the crypto library and 64 random bytes (converted to hex)
* for the token. When used in combination with the user.verify() method this
* function will be called with the `user` object as it's context (`this`).
*
* @param {object} user The User this token is being generated for.
* @param {Function} cb The generator must pass back the new token with this function call
*/
User.generateVerificationToken = function(user, cb) {
crypto.randomBytes(64, function(err, buf) {
cb(err, buf && buf.toString('hex'));
});
};
/**
* Confirm the user's identity.
*
* @param {Any} userId
* @param {String} token The validation token
* @param {String} redirect URL to redirect the user to once confirmed
* @callback {Function} callback
* @param {Error} err
* @promise
*/
User.confirm = function(uid, token, redirect, fn) {
fn = fn || utils.createPromiseCallback();
this.findById(uid, function(err, user) {
if (err) {
fn(err);
} else {
if (user && user.verificationToken === token) {
user.verificationToken = null;
user.emailVerified = true;
user.save(function(err) {
if (err) {
fn(err);
} else {
fn();
}
});
} else {
if (user) {
err = new Error(g.f('Invalid token: %s', token));
err.statusCode = 400;
err.code = 'INVALID_TOKEN';
} else {
err = new Error(g.f('User not found: %s', uid));
err.statusCode = 404;
err.code = 'USER_NOT_FOUND';
}
fn(err);
}
}
});
return fn.promise;
};
/**
* Create a short lived access token for temporary login. Allows users
* to change passwords if forgotten.
*
* @options {Object} options
* @prop {String} email The user's email address
* @property {String} realm The user's realm (optional)
* @callback {Function} callback
* @param {Error} err
* @promise
*/
User.resetPassword = function(options, cb) {
cb = cb || utils.createPromiseCallback();
var UserModel = this;
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
options = options || {};
if (typeof options.email !== 'string') {
var err = new Error(g.f('Email is required'));
err.statusCode = 400;
err.code = 'EMAIL_REQUIRED';
cb(err);
return cb.promise;
}
try {
if (options.password) {
UserModel.validatePassword(options.password);
}
} catch (err) {
return cb(err);
}
var where = {
email: options.email,
};
if (options.realm) {
where.realm = options.realm;
}
UserModel.findOne({where: where}, function(err, user) {
if (err) {
return cb(err);
}
if (!user) {
err = new Error(g.f('Email not found'));
err.statusCode = 404;
err.code = 'EMAIL_NOT_FOUND';
return cb(err);
}
// create a short lived access token for temp login to change password
// TODO(ritch) - eventually this should only allow password change
if (UserModel.settings.emailVerificationRequired && !user.emailVerified) {
err = new Error(g.f('Email has not been verified'));
err.statusCode = 401;
err.code = 'RESET_FAILED_EMAIL_NOT_VERIFIED';
return cb(err);
}
user.createAccessToken(ttl, function(err, accessToken) {
if (err) {
return cb(err);
}
cb();
UserModel.emit('resetPasswordRequest', {
email: options.email,
accessToken: accessToken,
user: user,
options: options,
});
});
});
return cb.promise;
};
/*!
* Hash the plain password
*/
User.hashPassword = function(plain) {
this.validatePassword(plain);
var salt = bcrypt.genSaltSync(this.settings.saltWorkFactor || SALT_WORK_FACTOR);
return bcrypt.hashSync(plain, salt);
};
User.validatePassword = function(plain) {
var err;
if (plain && typeof plain === 'string' && plain.length <= MAX_PASSWORD_LENGTH) {
return true;
}
if (plain.length > MAX_PASSWORD_LENGTH) {
err = new Error(g.f('Password too long: %s', plain));
err.code = 'PASSWORD_TOO_LONG';
} else {
err = new Error(g.f('Invalid password: %s', plain));
err.code = 'INVALID_PASSWORD';
}
err.statusCode = 422;
throw err;
};
User._invalidateAccessTokensOfUsers = function(userIds, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
if (!Array.isArray(userIds) || !userIds.length)
return process.nextTick(cb);
var accessTokenRelation = this.relations.accessTokens;
if (!accessTokenRelation)
return process.nextTick(cb);
var AccessToken = accessTokenRelation.modelTo;
var query = {userId: {inq: userIds}};
var tokenPK = AccessToken.definition.idName() || 'id';
if (options.accessToken && tokenPK in options.accessToken) {
query[tokenPK] = {neq: options.accessToken[tokenPK]};
}
// add principalType in AccessToken.query if using polymorphic relations
// between AccessToken and User
var relatedUser = AccessToken.relations.user;
var isRelationPolymorphic = relatedUser && relatedUser.polymorphic &&
!relatedUser.modelTo;
if (isRelationPolymorphic) {
query.principalType = this.modelName;
}
AccessToken.deleteAll(query, options, cb);
};
/*!
* Setup an extended user model.
*/
User.setup = function() {
// We need to call the base class's setup method
User.base.setup.call(this);
var UserModel = this;
// max ttl
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
this.settings.ttl = this.settings.ttl || DEFAULT_TTL;
UserModel.setter.email = function(value) {
if (!UserModel.settings.caseSensitiveEmail) {
this.$email = value.toLowerCase();
} else {
this.$email = value;
}
};
UserModel.setter.password = function(plain) {
if (typeof plain !== 'string') {
return;
}
if (plain.indexOf('$2a$') === 0 && plain.length === 60) {
// The password is already hashed. It can be the case
// when the instance is loaded from DB
this.$password = plain;
} else {
this.$password = this.constructor.hashPassword(plain);
}
};
// Make sure emailVerified is not set by creation
UserModel.beforeRemote('create', function(ctx, user, next) {
var body = ctx.req.body;
if (body && body.emailVerified) {
body.emailVerified = false;
}
next();
});
UserModel.remoteMethod(
'login',
{
description: 'Login a user with username/email and password.',
accepts: [
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
{arg: 'include', type: ['string'], http: {source: 'query'},
description: 'Related objects to include in the response. ' +
'See the description of return value for more details.'},
],
returns: {
arg: 'accessToken', type: 'object', root: true,
description:
g.f('The response body contains properties of the {{AccessToken}} created on login.\n' +
'Depending on the value of `include` parameter, the body may contain ' +
'additional properties:\n\n' +
' - `user` - `U+007BUserU+007D` - Data of the currently logged in user. ' +
'{{(`include=user`)}}\n\n'),
},
http: {verb: 'post'},
}
);
UserModel.remoteMethod(
'logout',
{
description: 'Logout a user with access token.',
accepts: [
{arg: 'access_token', type: 'string', http: function(ctx) {
var req = ctx && ctx.req;
var accessToken = req && req.accessToken;
var tokenID = accessToken ? accessToken.id : undefined;
return tokenID;
}, description: 'Do not supply this argument, it is automatically extracted ' +
'from request headers.',
},
],
http: {verb: 'all'},
}
);
UserModel.remoteMethod(
'confirm',
{
description: 'Confirm a user registration with email verification token.',
accepts: [
{arg: 'uid', type: 'string', required: true},
{arg: 'token', type: 'string', required: true},
{arg: 'redirect', type: 'string'},
],
http: {verb: 'get', path: '/confirm'},
}
);
UserModel.remoteMethod(
'resetPassword',
{
description: 'Reset password for a user with email.',
accepts: [
{arg: 'options', type: 'object', required: true, http: {source: 'body'}},
],
http: {verb: 'post', path: '/reset'},
}
);
UserModel.remoteMethod(
'changePassword',
{
description: 'Change a user\'s password.',
accepts: [
{arg: 'id', type: 'any',
http: ctx => ctx.req.accessToken && ctx.req.accessToken.userId,
},
{arg: 'oldPassword', type: 'string', required: true, http: {source: 'form'}},
{arg: 'newPassword', type: 'string', required: true, http: {source: 'form'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
http: {verb: 'POST', path: '/change-password'},
}
);
UserModel.afterRemote('confirm', function(ctx, inst, next) {
if (ctx.args.redirect !== undefined) {
if (!ctx.res) {
return next(new Error(g.f('The transport does not support HTTP redirects.')));
}
ctx.res.location(ctx.args.redirect);
ctx.res.status(302);
}
next();
});
// default models
assert(loopback.Email, 'Email model must be defined before User model');
UserModel.email = loopback.Email;
assert(loopback.AccessToken, 'AccessToken model must be defined before User model');
UserModel.accessToken = loopback.AccessToken;
UserModel.validate('email', emailValidator, {
message: g.f('Must provide a valid email'),
});
// Realm users validation
Iif (UserModel.settings.realmRequired && UserModel.settings.realmDelimiter) {
UserModel.validatesUniquenessOf('email', {
message: 'Email already exists',
scopedTo: ['realm'],
});
UserModel.validatesUniquenessOf('username', {
message: 'User already exists',
scopedTo: ['realm'],
});
} else {
// Regular(Non-realm) users validation
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
}
return UserModel;
};
/*!
* Setup the base user.
*/
User.setup();
// --- OPERATION HOOKS ---
//
// Important: Operation hooks are inherited by subclassed models,
// therefore they must be registered outside of setup() function
// Access token to normalize email credentials
User.observe('access', function normalizeEmailCase(ctx, next) {
if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where &&
ctx.query.where.email && typeof(ctx.query.where.email) === 'string') {
ctx.query.where.email = ctx.query.where.email.toLowerCase();
}
next();
});
User.observe('before save', function prepareForTokenInvalidation(ctx, next) {
if (ctx.isNewInstance) return next();
if (!ctx.where && !ctx.instance) return next();
var pkName = ctx.Model.definition.idName() || 'id';
var where = ctx.where;
if (!where) {
where = {};
where[pkName] = ctx.instance[pkName];
}
ctx.Model.find({where: where}, ctx.options, function(err, userInstances) {
if (err) return next(err);
ctx.hookState.originalUserData = userInstances.map(function(u) {
var user = {};
user[pkName] = u[pkName];
user.email = u.email;
user.password = u.password;
return user;
});
var emailChanged;
if (ctx.instance) {
emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email;
if (emailChanged && ctx.Model.settings.emailVerificationRequired) {
ctx.instance.emailVerified = false;
}
} else if (ctx.data.email) {
emailChanged = ctx.hookState.originalUserData.some(function(data) {
return data.email != ctx.data.email;
});
if (emailChanged && ctx.Model.settings.emailVerificationRequired) {
ctx.data.emailVerified = false;
}
}
next();
});
});
User.observe('after save', function invalidateOtherTokens(ctx, next) {
if (!ctx.instance && !ctx.data) return next();
if (!ctx.hookState.originalUserData) return next();
var pkName = ctx.Model.definition.idName() || 'id';
var newEmail = (ctx.instance || ctx.data).email;
var newPassword = (ctx.instance || ctx.data).password;
if (!newEmail && !newPassword) return next();
var userIdsToExpire = ctx.hookState.originalUserData.filter(function(u) {
return (newEmail && u.email !== newEmail) ||
(newPassword && u.password !== newPassword);
}).map(function(u) {
return u[pkName];
});
ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, ctx.options, next);
});
};
function emailValidator(err, done) {
var value = this.email;
if (value == null)
return;
if (typeof value !== 'string')
return err('string');
if (value === '') return;
if (!isEmail.validate(value))
return err('email');
}
function joinUrlPath(args) {
var result = arguments[0];
for (var ix = 1; ix < arguments.length; ix++) {
var next = arguments[ix];
result += result[result.length - 1] === '/' && next[0] === '/' ?
next.slice(1) : next;
}
return result;
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| access-context.js | 21.82% | (36 / 165) | 0% | (0 / 118) | 0% | (0 / 16) | 22.09% | (36 / 163) | |
| application.js | 13.69% | (33 / 241) | 0% | (0 / 161) | 0% | (0 / 31) | 14.1% | (33 / 234) | |
| browser-express.js | 56.25% | (9 / 16) | 0% | (0 / 2) | 0% | (0 / 4) | 56.25% | (9 / 16) | |
| builtin-models.js | 100% | (27 / 27) | 100% | (4 / 4) | 100% | (3 / 3) | 100% | (27 / 27) | |
| current-context.js | 70% | (7 / 10) | 100% | (0 / 0) | 25% | (1 / 4) | 70% | (7 / 10) | |
| globalize.js | 100% | (4 / 4) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (4 / 4) | |
| loopback.js | 58.75% | (47 / 80) | 13.04% | (3 / 23) | 31.58% | (6 / 19) | 59.49% | (47 / 79) | |
| model.js | 20.22% | (75 / 371) | 6.3% | (16 / 254) | 16.67% | (8 / 48) | 20.66% | (75 / 363) | |
| persisted-model.js | 21.52% | (136 / 632) | 3.05% | (9 / 295) | 3.13% | (4 / 128) | 22.44% | (136 / 606) | |
| registry.js | 37.58% | (56 / 149) | 16.35% | (17 / 104) | 40% | (6 / 15) | 36.99% | (54 / 146) | |
| runtime.js | 100% | (3 / 3) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (3 / 3) | |
| server-app.js | 13.18% | (17 / 129) | 0% | (0 / 64) | 0% | (0 / 15) | 13.71% | (17 / 124) | |
| utils.js | 19.35% | (12 / 62) | 0% | (0 / 32) | 0% | (0 / 12) | 19.67% | (12 / 61) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var loopback = require('./loopback');
var debug = require('debug')('loopback:security:access-context');
/**
* Access context represents the context for a request to access protected
* resources
*
* NOTE While the method expects an array of principals in the AccessContext instance/object,
* it also accepts a single principal defined with the following properties:
* ```js
* {
* // AccessContext instance/object
* // ..
* principalType: 'somePrincipalType', // APP, ROLE, USER, or custom user model name
* principalId: 'somePrincipalId',
* }
* ```
*
* @class
* @options {AccessContext|Object} context An AccessContext instance or an object
* @property {Principal[]} principals An array of principals
* @property {Function} model The model class
* @property {String} modelName The model name
* @property {*} modelId The model id
* @property {String} property The model property/method/relation name
* @property {String} method The model method to be invoked
* @property {String} accessType The access type: READ, REPLICATE, WRITE, or EXECUTE.
* @property {AccessToken} accessToken The access token resolved for the request
* @property {RemotingContext} remotingContext The request's remoting context
* @property {Registry} registry The application or global registry
* @returns {AccessContext}
* @constructor
*/
function AccessContext(context) {
if (!(this instanceof AccessContext)) {
return new AccessContext(context);
}
context = context || {};
assert(context.registry,
'Application registry is mandatory in AccessContext but missing in provided context');
this.registry = context.registry;
this.principals = context.principals || [];
var model = context.model;
model = ('string' === typeof model) ? this.registry.getModel(model) : model;
this.model = model;
this.modelName = model && model.modelName;
this.modelId = context.id || context.modelId;
this.property = context.property || AccessContext.ALL;
this.method = context.method;
this.sharedMethod = context.sharedMethod;
this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass;
if (this.sharedMethod) {
this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
} else {
this.methodNames = [];
}
if (this.sharedMethod) {
this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
}
this.accessType = context.accessType || AccessContext.ALL;
assert(loopback.AccessToken,
'AccessToken model must be defined before AccessContext model');
this.accessToken = context.accessToken || loopback.AccessToken.ANONYMOUS;
var principalType = context.principalType || Principal.USER;
var principalId = context.principalId || undefined;
var principalName = context.principalName || undefined;
if (principalId) {
this.addPrincipal(principalType, principalId, principalName);
}
var token = this.accessToken || {};
if (token.userId) {
this.addPrincipal(Principal.USER, token.userId);
}
if (token.appId) {
this.addPrincipal(Principal.APPLICATION, token.appId);
}
this.remotingContext = context.remotingContext;
}
// Define constant for the wildcard
AccessContext.ALL = '*';
// Define constants for access types
AccessContext.READ = 'READ'; // Read operation
AccessContext.REPLICATE = 'REPLICATE'; // Replicate (pull) changes
AccessContext.WRITE = 'WRITE'; // Write operation
AccessContext.EXECUTE = 'EXECUTE'; // Execute operation
AccessContext.DEFAULT = 'DEFAULT'; // Not specified
AccessContext.ALLOW = 'ALLOW'; // Allow
AccessContext.ALARM = 'ALARM'; // Warn - send an alarm
AccessContext.AUDIT = 'AUDIT'; // Audit - record the access
AccessContext.DENY = 'DENY'; // Deny
AccessContext.permissionOrder = {
DEFAULT: 0,
ALLOW: 1,
ALARM: 2,
AUDIT: 3,
DENY: 4,
};
/**
* Add a principal to the context
* @param {String} principalType The principal type
* @param {*} principalId The principal id
* @param {String} [principalName] The principal name
* @returns {boolean}
*/
AccessContext.prototype.addPrincipal = function(principalType, principalId, principalName) {
var principal = new Principal(principalType, principalId, principalName);
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
if (p.equals(principal)) {
return false;
}
}
this.principals.push(principal);
return true;
};
/**
* Get the user id
* @returns {*}
*/
AccessContext.prototype.getUserId = function() {
var user = this.getUser();
return user && user.id;
};
/**
* Get the user
* @returns {*}
*/
AccessContext.prototype.getUser = function() {
var BaseUser = this.registry.getModel('User');
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
var isBuiltinPrincipal = p.type === Principal.APP ||
p.type === Principal.ROLE ||
p.type == Principal.SCOPE;
if (isBuiltinPrincipal) continue;
// the principalType must either be 'USER'
if (p.type === Principal.USER) {
return {id: p.id, principalType: p.type};
}
// or permit to resolve a valid user model
var userModel = this.registry.findModel(p.type);
if (!userModel) continue;
if (userModel.prototype instanceof BaseUser) {
return {id: p.id, principalType: p.type};
}
}
};
/**
* Get the application id
* @returns {*}
*/
AccessContext.prototype.getAppId = function() {
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
if (p.type === Principal.APPLICATION) {
return p.id;
}
}
return null;
};
/**
* Check if the access context has authenticated principals
* @returns {boolean}
*/
AccessContext.prototype.isAuthenticated = function() {
return !!(this.getUserId() || this.getAppId());
};
/*!
* Print debug info for access context.
*/
AccessContext.prototype.debug = function() {
if (debug.enabled) {
debug('---AccessContext---');
if (this.principals && this.principals.length) {
debug('principals:');
this.principals.forEach(function(principal) {
debug('principal: %j', principal);
});
} else {
debug('principals: %j', this.principals);
}
debug('modelName %s', this.modelName);
debug('modelId %s', this.modelId);
debug('property %s', this.property);
debug('method %s', this.method);
debug('accessType %s', this.accessType);
if (this.accessToken) {
debug('accessToken:');
debug(' id %j', this.accessToken.id);
debug(' ttl %j', this.accessToken.ttl);
}
debug('getUserId() %s', this.getUserId());
debug('isAuthenticated() %s', this.isAuthenticated());
}
};
/**
* This class represents the abstract notion of a principal, which can be used
* to represent any entity, such as an individual, a corporation, and a login id
* @param {String} type The principal type
* @param {*} id The principal id
* @param {String} [name] The principal name
* @param {String} modelName The principal model name
* @returns {Principal}
* @class
*/
function Principal(type, id, name) {
if (!(this instanceof Principal)) {
return new Principal(type, id, name);
}
this.type = type;
this.id = id;
this.name = name;
}
// Define constants for principal types
Principal.USER = 'USER';
Principal.APP = Principal.APPLICATION = 'APP';
Principal.ROLE = 'ROLE';
Principal.SCOPE = 'SCOPE';
/**
* Compare if two principals are equal
* Returns true if argument principal is equal to this principal.
* @param {Object} p The other principal
*/
Principal.prototype.equals = function(p) {
if (p instanceof Principal) {
return this.type === p.type && String(this.id) === String(p.id);
}
return false;
};
/**
* A request to access protected resources.
*
* The method can either be called with the following signature or with a single
* argument: an AccessRequest instance or an object containing all the required properties.
*
* @class
* @options {String|AccessRequest|Object} model|req The model name,<br>
* or an AccessRequest instance/object.
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
* @param {String} permission The requested permission
* @param {String[]} methodNames The names of involved methods
* @param {Registry} registry The application or global registry
* @returns {AccessRequest}
*/
function AccessRequest(model, property, accessType, permission, methodNames, registry) {
if (!(this instanceof AccessRequest)) {
return new AccessRequest(model, property, accessType, permission, methodNames);
}
if (arguments.length === 1 && typeof model === 'object') {
// The argument is an object that contains all required properties
var obj = model || {};
this.model = obj.model || AccessContext.ALL;
this.property = obj.property || AccessContext.ALL;
this.accessType = obj.accessType || AccessContext.ALL;
this.permission = obj.permission || AccessContext.DEFAULT;
this.methodNames = obj.methodNames || [];
this.registry = obj.registry;
} else {
this.model = model || AccessContext.ALL;
this.property = property || AccessContext.ALL;
this.accessType = accessType || AccessContext.ALL;
this.permission = permission || AccessContext.DEFAULT;
this.methodNames = methodNames || [];
this.registry = registry;
}
// do not create AccessRequest without a registry
assert(this.registry,
'Application registry is mandatory in AccessRequest but missing in provided argument(s)');
}
/**
* Does the request contain any wildcards?
*
* @returns {Boolean}
*/
AccessRequest.prototype.isWildcard = function() {
return this.model === AccessContext.ALL ||
this.property === AccessContext.ALL ||
this.accessType === AccessContext.ALL;
};
/**
* Does the given `ACL` apply to this `AccessRequest`.
*
* @param {ACL} acl
*/
AccessRequest.prototype.exactlyMatches = function(acl) {
var matchesModel = acl.model === this.model;
var matchesProperty = acl.property === this.property;
var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
var matchesAccessType = acl.accessType === this.accessType;
if (matchesModel && matchesAccessType) {
return matchesProperty || matchesMethodName;
}
return false;
};
/**
* Settle the accessRequest's permission if DEFAULT
* In most situations, the default permission can be resolved from the nested model
* config. An default permission can also be explicitly provided to override it or
* cope with AccessRequest instances without a nested model (e.g. model is '*')
*
* @param {String} defaultPermission (optional) the default permission to apply
*/
AccessRequest.prototype.settleDefaultPermission = function(defaultPermission) {
if (this.permission !== 'DEFAULT')
return;
var modelName = this.model;
if (!defaultPermission) {
var modelClass = this.registry.findModel(modelName);
defaultPermission = modelClass && modelClass.settings.defaultPermission;
}
this.permission = defaultPermission || 'ALLOW';
};
/**
* Is the request for access allowed?
*
* @returns {Boolean}
*/
AccessRequest.prototype.isAllowed = function() {
return this.permission !== loopback.ACL.DENY;
};
AccessRequest.prototype.debug = function() {
if (debug.enabled) {
debug('---AccessRequest---');
debug(' model %s', this.model);
debug(' property %s', this.property);
debug(' accessType %s', this.accessType);
debug(' permission %s', this.permission);
debug(' isWildcard() %s', this.isWildcard());
debug(' isAllowed() %s', this.isAllowed());
}
};
module.exports.AccessContext = AccessContext;
module.exports.Principal = Principal;
module.exports.AccessRequest = AccessRequest;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module dependencies.
*/
'use strict';
var g = require('./globalize');
var DataSource = require('loopback-datasource-juggler').DataSource;
var Registry = require('./registry');
var assert = require('assert');
var fs = require('fs');
var extend = require('util')._extend;
var RemoteObjects = require('strong-remoting');
var classify = require('underscore.string/classify');
var camelize = require('underscore.string/camelize');
var path = require('path');
var util = require('util');
/**
* The `App` object represents a Loopback application.
*
* The App object extends [Express](http://expressjs.com/api.html#express) and
* supports Express middleware. See
* [Express documentation](http://expressjs.com/) for details.
*
* ```js
* var loopback = require('loopback');
* var app = loopback();
*
* app.get('/', function(req, res){
* res.send('hello world');
* });
*
* app.listen(3000);
* ```
*
* @class LoopBackApplication
* @header var app = loopback()
*/
function App() {
// this is a dummy placeholder for jsdox
}
/*!
* Export the app prototype.
*/
var app = module.exports = {};
/**
* Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
*
* **NOTE:** Calling `app.remotes()` more than once returns only a single set of remote objects.
* @returns {RemoteObjects}
*/
app.remotes = function() {
if (this._remotes) {
return this._remotes;
} else {
var options = {};
if (this.get) {
options = this.get('remoting');
}
return (this._remotes = RemoteObjects.create(options));
}
};
/*!
* Remove a route by reference.
*/
app.disuse = function(route) {
if (this.stack) {
for (var i = 0; i < this.stack.length; i++) {
if (this.stack[i].route === route) {
this.stack.splice(i, 1);
}
}
}
};
/**
* Attach a model to the app. The `Model` will be available on the
* `app.models` object.
*
* Example - Attach an existing model:
```js
* var User = loopback.User;
* app.model(User);
*```
* Example - Attach an existing model, alter some aspects of the model:
* ```js
* var User = loopback.User;
* app.model(User, { dataSource: 'db' });
*```
*
* @param {Object} Model The model to attach.
* @options {Object} config The model's configuration.
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
* @property {Boolean} [public] Whether the model should be exposed via REST API.
* @property {Object} [relations] Relations to add/update.
* @end
* @returns {ModelConstructor} the model class
*/
app.model = function(Model, config) {
var isPublic = true;
var registry = this.registry;
if (typeof Model === 'string') {
var msg = 'app.model(modelName, settings) is no longer supported. ' +
'Use app.registry.createModel(modelName, definition) and ' +
'app.model(ModelCtor, config) instead.';
throw new Error(msg);
}
if (arguments.length > 1) {
config = config || {};
configureModel(Model, config, this);
isPublic = config.public !== false;
} else {
assert(Model.prototype instanceof Model.registry.getModel('Model'),
Model.modelName + ' must be a descendant of loopback.Model');
}
var modelName = Model.modelName;
this.models[modelName] =
this.models[classify(modelName)] =
this.models[camelize(modelName)] = Model;
this.models().push(Model);
if (isPublic && Model.sharedClass) {
this.remotes().defineObjectType(Model.modelName, function(data) {
return new Model(data);
});
this.remotes().addClass(Model.sharedClass);
if (Model.settings.trackChanges && Model.Change) {
this.remotes().addClass(Model.Change.sharedClass);
}
clearHandlerCache(this);
this.emit('modelRemoted', Model.sharedClass);
}
var self = this;
Model.on('remoteMethodDisabled', function(model, methodName) {
self.emit('remoteMethodDisabled', model, methodName);
});
Model.on('remoteMethodAdded', function(model) {
self.emit('remoteMethodAdded', model);
});
Model.shared = isPublic;
Model.app = this;
Model.emit('attached', this);
return Model;
};
/**
* Get the models exported by the app. Returns only models defined using `app.model()`
*
* There are two ways to access models:
*
* 1. Call `app.models()` to get a list of all models.
*
* ```js
* var models = app.models();
*
* models.forEach(function(Model) {
* console.log(Model.modelName); // color
* });
* ```
*
* 2. Use `app.models` to access a model by name.
* `app.models` has properties for all defined models.
*
* The following example illustrates accessing the `Product` and `CustomerReceipt` models
* using the `models` object.
*
* ```js
* var loopback = require('loopback');
* var app = loopback();
* app.boot({
* dataSources: {
* db: {connector: 'memory'}
* }
* });
*
* var productModel = app.registry.createModel('product');
* app.model(productModel, {dataSource: 'db'});
* var customerReceiptModel = app.registry.createModel('customer-receipt');
* app.model(customerReceiptModel, {dataSource: 'db'});
*
* // available based on the given name
* var Product = app.models.Product;
*
* // also available as camelCase
* var product = app.models.product;
*
* // multi-word models are avaiable as pascal cased
* var CustomerReceipt = app.models.CustomerReceipt;
*
* // also available as camelCase
* var customerReceipt = app.models.customerReceipt;
* ```
*
* @returns {Array} Array of model classes.
*/
app.models = function() {
return this._models || (this._models = []);
};
/**
* Define a DataSource.
*
* @param {String} name The data source name
* @param {Object} config The data source config
*/
app.dataSource = function(name, config) {
try {
var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
this.dataSources[name] =
this.dataSources[classify(name)] =
this.dataSources[camelize(name)] = ds;
ds.app = this;
return ds;
} catch (err) {
if (err.message) {
err.message = g.f('Cannot create data source %s: %s',
JSON.stringify(name), err.message);
}
throw err;
}
};
/**
* Register a connector.
*
* When a new data-source is being added via `app.dataSource`, the connector
* name is looked up in the registered connectors first.
*
* Connectors are required to be explicitly registered only for applications
* using browserify, because browserify does not support dynamic require,
* which is used by LoopBack to automatically load the connector module.
*
* @param {String} name Name of the connector, e.g. 'mysql'.
* @param {Object} connector Connector object as returned
* by `require('loopback-connector-{name}')`.
*/
app.connector = function(name, connector) {
this.connectors[name] =
this.connectors[classify(name)] =
this.connectors[camelize(name)] = connector;
};
/**
* Get all remote objects.
* @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
*/
app.remoteObjects = function() {
var result = {};
this.remotes().classes().forEach(function(sharedClass) {
result[sharedClass.name] = sharedClass.ctor;
});
return result;
};
/*!
* Get a handler of the specified type from the handler cache.
* @triggers `mounted` events on shared class constructors (models)
*/
app.handler = function(type, options) {
var handlers = this._handlers || (this._handlers = {});
if (handlers[type]) {
return handlers[type];
}
var remotes = this.remotes();
var handler = this._handlers[type] = remotes.handler(type, options);
remotes.classes().forEach(function(sharedClass) {
sharedClass.ctor.emit('mounted', app, sharedClass, remotes);
});
return handler;
};
/**
* An object to store dataSource instances.
*/
app.dataSources = app.datasources = {};
/**
* Enable app wide authentication.
*/
app.enableAuth = function(options) {
var AUTH_MODELS = ['User', 'AccessToken', 'ACL', 'Role', 'RoleMapping'];
var remotes = this.remotes();
var app = this;
if (options && options.dataSource) {
var appModels = app.registry.modelBuilder.models;
AUTH_MODELS.forEach(function(m) {
var Model = app.registry.findModel(m);
if (!Model) {
throw new Error(
g.f('Authentication requires model %s to be defined.', m));
}
if (Model.dataSource || Model.app) return;
// Find descendants of Model that are attached,
// for example "Customer" extending "User" model
for (var name in appModels) {
var candidate = appModels[name];
var isSubclass = candidate.prototype instanceof Model;
var isAttached = !!candidate.dataSource || !!candidate.app;
if (isSubclass && isAttached) return;
}
app.model(Model, {
dataSource: options.dataSource,
public: m === 'User',
});
});
}
remotes.authorization = function(ctx, next) {
var method = ctx.method;
var req = ctx.req;
var Model = method.ctor;
var modelInstance = ctx.instance;
var modelId = modelInstance && modelInstance.id ||
// replacement for deprecated req.param()
(req.params && req.params.id !== undefined ? req.params.id :
req.body && req.body.id !== undefined ? req.body.id :
req.query && req.query.id !== undefined ? req.query.id :
undefined);
var modelName = Model.modelName;
var modelSettings = Model.settings || {};
var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401;
if (!req.accessToken) {
errStatusCode = 401;
}
if (Model.checkAccess) {
Model.checkAccess(
req.accessToken,
modelId,
method,
ctx,
function(err, allowed) {
if (err) {
console.log(err);
next(err);
} else if (allowed) {
next();
} else {
var messages = {
403: {
message: g.f('Access Denied'),
code: 'ACCESS_DENIED',
},
404: {
message: (g.f('could not find %s with id %s', modelName, modelId)),
code: 'MODEL_NOT_FOUND',
},
401: {
message: g.f('Authorization Required'),
code: 'AUTHORIZATION_REQUIRED',
},
};
var e = new Error(messages[errStatusCode].message || messages[403].message);
e.statusCode = errStatusCode;
e.code = messages[errStatusCode].code || messages[403].code;
next(e);
}
}
);
} else {
next();
}
};
this._verifyAuthModelRelations();
this.isAuthEnabled = true;
};
app._verifyAuthModelRelations = function() {
// Allow unit-tests (but also LoopBack users) to disable the warnings
if (this.get('_verifyAuthModelRelations') === false) return;
const AccessToken = this.registry.findModel('AccessToken');
const User = this.registry.findModel('User');
this.models().forEach(Model => {
if (Model === AccessToken || Model.prototype instanceof AccessToken) {
scheduleVerification(Model, verifyAccessTokenRelations);
}
if (Model === User || Model.prototype instanceof User) {
scheduleVerification(Model, verifyUserRelations);
}
});
function scheduleVerification(Model, verifyFn) {
if (Model.dataSource) {
verifyFn(Model);
} else {
Model.on('attached', () => verifyFn(Model));
}
}
function verifyAccessTokenRelations(Model) {
const belongsToUser = Model.relations && Model.relations.user;
if (belongsToUser) return;
const relationsConfig = Model.settings.relations || {};
const userName = (relationsConfig.user || {}).model;
if (userName) {
console.warn(
'The model %j configures "belongsTo User-like models" relation ' +
'with target model %j. However, the model %j is not attached to ' +
'the application and therefore cannot be used by this relation. ' +
'This typically happens when the application has a custom ' +
'custom User subclass, but does not fix AccessToken relations ' +
'to use this new model.\n' +
'Learn more at http://ibm.biz/setup-loopback-auth',
Model.modelName, userName, userName);
return;
}
console.warn(
'The model %j does not have "belongsTo User-like model" relation ' +
'configured.\n' +
'Learn more at http://ibm.biz/setup-loopback-auth',
Model.modelName);
}
function verifyUserRelations(Model) {
const hasManyTokens = Model.relations && Model.relations.accessTokens;
if (hasManyTokens) return;
const relationsConfig = Model.settings.relations || {};
const accessTokenName = (relationsConfig.accessTokens || {}).model;
if (accessTokenName) {
console.warn(
'The model %j configures "hasMany AccessToken-like models" relation ' +
'with target model %j. However, the model %j is not attached to ' +
'the application and therefore cannot be used by this relation. ' +
'This typically happens when the application has a custom ' +
'AccessToken subclass, but does not fix User relations to use this ' +
'new model.\n' +
'Learn more at http://ibm.biz/setup-loopback-auth',
Model.modelName, accessTokenName, accessTokenName);
return;
}
console.warn(
'The model %j does not have "hasMany AccessToken-like models" relation ' +
'configured.\n' +
'Learn more at http://ibm.biz/setup-loopback-auth',
Model.modelName);
}
};
app.boot = function(options) {
throw new Error(
g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead'));
};
function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
var connectorPath;
assert(typeof config === 'object',
'can not create data source without config object');
if (typeof config.connector === 'string') {
name = config.connector;
if (connectorRegistry[name]) {
config.connector = connectorRegistry[name];
} else {
connectorPath = path.join(__dirname, 'connectors', name + '.js');
if (fs.existsSync(connectorPath)) {
config.connector = require(connectorPath);
}
}
if (config.connector && typeof config.connector === 'object' && !config.connector.name)
config.connector.name = name;
}
return registry.createDataSource(config);
}
function configureModel(ModelCtor, config, app) {
assert(ModelCtor.prototype instanceof ModelCtor.registry.getModel('Model'),
ModelCtor.modelName + ' must be a descendant of loopback.Model');
var dataSource = config.dataSource;
if (dataSource) {
if (typeof dataSource === 'string') {
dataSource = app.dataSources[dataSource];
}
assert(
dataSource instanceof DataSource,
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
config.dataSource + '"'
);
}
config = extend({}, config);
config.dataSource = dataSource;
setSharedMethodSharedProperties(ModelCtor, app, config);
app.registry.configureModel(ModelCtor, config);
}
function setSharedMethodSharedProperties(model, app, modelConfigs) {
var settings = {};
// apply config.json settings
var config = app.get('remoting');
var configHasSharedMethodsSettings = config &&
config.sharedMethods &&
typeof config.sharedMethods === 'object';
if (configHasSharedMethodsSettings)
util._extend(settings, config.sharedMethods);
// apply model-config.json settings
var modelConfig = modelConfigs.options;
var modelConfigHasSharedMethodsSettings = modelConfig &&
modelConfig.remoting &&
modelConfig.remoting.sharedMethods &&
typeof modelConfig.remoting.sharedMethods === 'object';
if (modelConfigHasSharedMethodsSettings)
util._extend(settings, modelConfig.remoting.sharedMethods);
// validate setting values
Object.keys(settings).forEach(function(setting) {
var settingValue = settings[setting];
var settingValueType = typeof settingValue;
if (settingValueType !== 'boolean')
throw new TypeError(g.f('Expected boolean, got %s', settingValueType));
});
// set sharedMethod.shared using the merged settings
var sharedMethods = model.sharedClass.methods({includeDisabled: true});
sharedMethods.forEach(function(sharedMethod) {
// use the specific setting if it exists
var hasSpecificSetting = settings.hasOwnProperty(sharedMethod.name);
if (hasSpecificSetting) {
sharedMethod.shared = settings[sharedMethod.name];
} else { // otherwise, use the default setting if it exists
var hasDefaultSetting = settings.hasOwnProperty('*');
if (hasDefaultSetting)
sharedMethod.shared = settings['*'];
}
});
}
function clearHandlerCache(app) {
app._handlers = undefined;
}
/**
* Listen for connections and update the configured port.
*
* When there are no parameters or there is only one callback parameter,
* the server will listen on `app.get('host')` and `app.get('port')`.
*
* For example, to listen on host/port configured in app config:
* ```js
* app.listen();
* ```
*
* Otherwise all arguments are forwarded to `http.Server.listen`.
*
* For example, to listen on the specified port and all hosts, and ignore app config.
* ```js
* app.listen(80);
* ```
*
* The function also installs a `listening` callback that calls
* `app.set('port')` with the value returned by `server.address().port`.
* This way the port param contains always the real port number, even when
* listen was called with port number 0.
*
* @param {Function} [cb] If specified, the callback is added as a listener
* for the server's "listening" event.
* @returns {http.Server} A node `http.Server` with this application configured
* as the request handler.
*/
app.listen = function(cb) {
var self = this;
var server = require('http').createServer(this);
server.on('listening', function() {
self.set('port', this.address().port);
var listeningOnAll = false;
var host = self.get('host');
if (!host) {
listeningOnAll = true;
host = this.address().address;
self.set('host', host);
} else if (host === '0.0.0.0' || host === '::') {
listeningOnAll = true;
}
if (!self.get('url')) {
if (process.platform === 'win32' && listeningOnAll) {
// Windows browsers don't support `0.0.0.0` host in the URL
// We are replacing it with localhost to build a URL
// that can be copied and pasted into the browser.
host = 'localhost';
}
var url = 'http://' + host + ':' + self.get('port') + '/';
self.set('url', url);
}
});
var useAppConfig =
arguments.length === 0 ||
(arguments.length == 1 && typeof arguments[0] == 'function');
if (useAppConfig) {
var port = this.get('port');
// NOTE(bajtos) port:undefined no longer works on node@6,
// we must pass port:0 explicitly
if (port === undefined) port = 0;
server.listen(port, this.get('host'), cb);
} else {
server.listen.apply(server, arguments);
}
return server;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var EventEmitter = require('events').EventEmitter;
var util = require('util');
module.exports = browserExpress;
function browserExpress() {
return new BrowserExpress();
}
browserExpress.errorHandler = {};
function BrowserExpress() {
this.settings = {};
}
util.inherits(BrowserExpress, EventEmitter);
BrowserExpress.prototype.set = function(key, value) {
if (arguments.length == 1) {
return this.get(key);
}
this.settings[key] = value;
return this; // fluent API
};
BrowserExpress.prototype.get = function(key) {
return this.settings[key];
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 11 11 11 11 1 111 111 111 302 302 100 202 111 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const assert = require('assert');
module.exports = function(registry) {
// NOTE(bajtos) we must use static require() due to browserify limitations
registry.KeyValueModel = createModel(
require('../common/models/key-value-model.json'),
require('../common/models/key-value-model.js'));
registry.Email = createModel(
require('../common/models/email.json'),
require('../common/models/email.js'));
registry.Application = createModel(
require('../common/models/application.json'),
require('../common/models/application.js'));
registry.AccessToken = createModel(
require('../common/models/access-token.json'),
require('../common/models/access-token.js'));
registry.User = createModel(
require('../common/models/user.json'),
require('../common/models/user.js'));
registry.RoleMapping = createModel(
require('../common/models/role-mapping.json'),
require('../common/models/role-mapping.js'));
registry.Role = createModel(
require('../common/models/role.json'),
require('../common/models/role.js'));
registry.ACL = createModel(
require('../common/models/acl.json'),
require('../common/models/acl.js'));
registry.Scope = createModel(
require('../common/models/scope.json'),
require('../common/models/scope.js'));
registry.Change = createModel(
require('../common/models/change.json'),
require('../common/models/change.js'));
registry.Checkpoint = createModel(
require('../common/models/checkpoint.json'),
require('../common/models/checkpoint.js'));
function createModel(definitionJson, customizeFn) {
// Clone the JSON definition to allow applications
// to modify model settings while not affecting
// settings of new models created in the local registry
// of another app.
// This is needed because require() always returns the same
// object instance it loaded during the first call.
definitionJson = cloneDeepJson(definitionJson);
var Model = registry.createModel(definitionJson);
customizeFn(Model);
return Model;
}
};
// Because we are cloning objects created by JSON.parse,
// the cloning algorithm can stay much simpler than a general-purpose
// "cloneDeep" e.g. from lodash.
function cloneDeepJson(obj) {
const result = Array.isArray(obj) ? [] : {};
assert.equal(Object.getPrototypeOf(result), Object.getPrototypeOf(obj));
for (const key in obj) {
const value = obj[key];
if (typeof value === 'object') {
result[key] = cloneDeepJson(value);
} else {
result[key] = value;
}
}
return result;
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('./globalize');
var juggler = require('loopback-datasource-juggler');
var remoting = require('strong-remoting');
module.exports = function(loopback) {
juggler.getCurrentContext =
remoting.getCurrentContext =
loopback.getCurrentContext = function() {
throw new Error(g.f(
'%s was removed in version 3.0. See %s for more details.',
'loopback.getCurrentContext()',
'http://loopback.io/doc/en/lb2/Using-current-context.html'));
};
loopback.runInContext = function(fn) {
throw new Error(g.f(
'%s was removed in version 3.0. See %s for more details.',
'loopback.runInContext()',
'http://loopback.io/doc/en/lb2/Using-current-context.html'));
};
loopback.createContext = function(scopeName) {
throw new Error(g.f(
'%s was removed in version 3.0. See %s for more details.',
'loopback.createContext()',
'http://loopback.io/doc/en/lb2/Using-current-context.html'));
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | 1 1 1 1 | // Copyright IBM Corp. 2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var path = require('path');
var SG = require('strong-globalize');
SG.SetRootDir(path.join(__dirname, '..'), {autonomousMsgLoading: 'all'});
module.exports = SG();
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 9 9 9 1 1 1 1 8 8 1 1 1 1 1 1 1 1 1 1 11 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module dependencies.
*/
'use strict';
var express = require('express');
var loopbackExpress = require('./server-app');
var proto = require('./application');
var fs = require('fs');
var ejs = require('ejs');
var path = require('path');
var merge = require('util')._extend;
var assert = require('assert');
var Registry = require('./registry');
var juggler = require('loopback-datasource-juggler');
/**
* LoopBack core module. It provides static properties and
* methods to create models and data sources. The module itself is a function
* that creates loopback `app`. For example:
*
* ```js
* var loopback = require('loopback');
* var app = loopback();
* ```
*
* @property {String} version Version of LoopBack framework. Static read-only property.
* @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property.
* @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property.
* @property {Registry} registry The global `Registry` object.
* @property {String} faviconFile Path to a default favicon shipped with LoopBack.
* Use as follows: `app.use(require('serve-favicon')(loopback.faviconFile));`
* @class loopback
* @header loopback
*/
var loopback = module.exports = createApplication;
/*!
* Framework version.
*/
loopback.version = require('../package.json').version;
loopback.registry = new Registry();
Object.defineProperties(loopback, {
Model: {
get: function() { return this.registry.getModel('Model'); },
},
PersistedModel: {
get: function() { return this.registry.getModel('PersistedModel'); },
},
defaultDataSources: {
get: function() { return this.registry.defaultDataSources; },
},
modelBuilder: {
get: function() { return this.registry.modelBuilder; },
},
});
/*!
* Create an loopback application.
*
* @return {Function}
* @api public
*/
function createApplication(options) {
var app = loopbackExpress();
merge(app, proto);
app.loopback = loopback;
// Create a new instance of models registry per each app instance
app.models = function() {
return proto.models.apply(this, arguments);
};
// Create a new instance of datasources registry per each app instance
app.datasources = app.dataSources = {};
// Create a new instance of connector registry per each app instance
app.connectors = {};
// Register built-in connectors. It's important to keep this code
// hand-written, so that all require() calls are static
// and thus browserify can process them (include connectors in the bundle)
app.connector('memory', loopback.Memory);
app.connector('remote', loopback.Remote);
app.connector('kv-memory',
require('loopback-datasource-juggler/lib/connectors/kv-memory'));
if (loopback.localRegistry || options && options.localRegistry === true) {
// setup the app registry
var registry = app.registry = new Registry();
if (options && options.loadBuiltinModels === true) {
require('./builtin-models')(registry);
}
} else {
app.registry = loopback.registry;
}
return app;
}
function mixin(source) {
for (var key in source) {
var desc = Object.getOwnPropertyDescriptor(source, key);
// Fix for legacy (pre-ES5) browsers like PhantomJS
Iif (!desc) continue;
Object.defineProperty(loopback, key, desc);
}
}
mixin(require('./runtime'));
/*!
* Expose static express methods like `express.Router`.
*/
mixin(express);
/*!
* Expose additional loopback middleware
* for example `loopback.configure` etc.
*
* ***only in node***
*/
Eif (loopback.isServer) {
fs
.readdirSync(path.join(__dirname, '..', 'server', 'middleware'))
.filter(function(file) {
return file.match(/\.js$/);
})
.forEach(function(m) {
loopback[m.replace(/\.js$/, '')] = require('../server/middleware/' + m);
});
loopback.urlNotFound = loopback['url-not-found'];
delete loopback['url-not-found'];
loopback.errorHandler = loopback['error-handler'];
delete loopback['error-handler'];
}
// Expose path to the default favicon file
// ***only in node***
Eif (loopback.isServer) {
/*!
* Path to a default favicon shipped with LoopBack.
*
* **Example**
*
* ```js
* app.use(require('serve-favicon')(loopback.faviconFile));
* ```
*/
loopback.faviconFile = path.resolve(__dirname, '../favicon.ico');
}
/**
* Add a remote method to a model.
* @param {Function} fn
* @param {Object} options (optional)
*/
loopback.remoteMethod = function(fn, options) {
fn.shared = true;
if (typeof options === 'object') {
Object.keys(options).forEach(function(key) {
fn[key] = options[key];
});
}
fn.http = fn.http || {verb: 'get'};
};
/**
* Create a template helper.
*
* var render = loopback.template('foo.ejs');
* var html = render({foo: 'bar'});
*
* @param {String} path Path to the template file.
* @returns {Function}
*/
loopback.template = function(file) {
var templates = this._templates || (this._templates = {});
var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
return ejs.compile(str, {
filename: file,
});
};
require('../lib/current-context')(loopback);
/**
* Create a named vanilla JavaScript class constructor with an attached
* set of properties and options.
*
* This function comes with two variants:
* * `loopback.createModel(name, properties, options)`
* * `loopback.createModel(config)`
*
* In the second variant, the parameters `name`, `properties` and `options`
* are provided in the config object. Any additional config entries are
* interpreted as `options`, i.e. the following two configs are identical:
*
* ```js
* { name: 'Customer', base: 'User' }
* { name: 'Customer', options: { base: 'User' } }
* ```
*
* **Example**
*
* Create an `Author` model using the three-parameter variant:
*
* ```js
* loopback.createModel(
* 'Author',
* {
* firstName: 'string',
* lastName: 'string'
* },
* {
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* }
* );
* ```
*
* Create the same model using a config object:
*
* ```js
* loopback.createModel({
* name: 'Author',
* properties: {
* firstName: 'string',
* lastName: 'string'
* },
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* });
* ```
*
* @param {String} name Unique name.
* @param {Object} properties
* @param {Object} options (optional)
*
* @header loopback.createModel
*/
loopback.createModel = function(name, properties, options) {
return this.registry.createModel.apply(this.registry, arguments);
};
/**
* Alter an existing Model class.
* @param {Model} ModelCtor The model constructor to alter.
* @options {Object} config Additional configuration to apply
* @property {DataSource} dataSource Attach the model to a dataSource.
* @property {Object} [relations] Model relations to add/update.
*
* @header loopback.configureModel(ModelCtor, config)
*/
loopback.configureModel = function(ModelCtor, config) {
return this.registry.configureModel.apply(this.registry, arguments);
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`
* @param {String} modelName The model name
* @returns {Model} The model class
*
* @header loopback.findModel(modelName)
*/
loopback.findModel = function(modelName) {
return this.registry.findModel.apply(this.registry, arguments);
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`. Throw an error when no such model exists.
*
* @param {String} modelName The model name
* @returns {Model} The model class
*
* @header loopback.getModel(modelName)
*/
loopback.getModel = function(modelName) {
return this.registry.getModel.apply(this.registry, arguments);
};
/**
* Look up a model class by the base model class.
* The method can be used by LoopBack
* to find configured models in models.json over the base model.
* @param {Model} modelType The base model class
* @returns {Model} The subclass if found or the base class
*
* @header loopback.getModelByType(modelType)
*/
loopback.getModelByType = function(modelType) {
return this.registry.getModelByType.apply(this.registry, arguments);
};
/**
* Create a data source with passing the provided options to the connector.
*
* @param {String} name Optional name.
* @options {Object} options Data Source options
* @property {Object} connector LoopBack connector.
* @property {*} [*] Other connector properties.
* See the relevant connector documentation.
*/
loopback.createDataSource = function(name, options) {
return this.registry.createDataSource.apply(this.registry, arguments);
};
/**
* Get an in-memory data source. Use one if it already exists.
*
* @param {String} [name] The name of the data source.
* If not provided, the `'default'` is used.
*/
loopback.memory = function(name) {
return this.registry.memory.apply(this.registry, arguments);
};
/*!
* Built in models / services
*/
require('./builtin-models')(loopback);
loopback.DataSource = juggler.DataSource;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 | 1 1 1 1 1 1 1 1 1 1 1 1 16 16 16 16 16 16 16 16 16 16 16 16 16 16 1 1 16 1 1 16 16 2 2 2 16 16 16 1 1 1 1 1 1 185 5 5 5 185 185 185 185 185 1 185 12 185 407 169 169 407 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module Dependencies.
*/
'use strict';
var g = require('./globalize');
var assert = require('assert');
var debug = require('debug')('loopback:model');
var RemoteObjects = require('strong-remoting');
var SharedClass = require('strong-remoting').SharedClass;
var extend = require('util')._extend;
var format = require('util').format;
var deprecated = require('depd')('loopback');
module.exports = function(registry) {
/**
* The base class for **all models**.
*
* **Inheriting from `Model`**
*
* ```js
* var properties = {...};
* var options = {...};
* var MyModel = loopback.Model.extend('MyModel', properties, options);
* ```
*
* **Options**
*
* - `trackChanges` - If true, changes to the model will be tracked. **Required
* for replication.**
*
* **Events**
*
* #### Event: `changed`
*
* Emitted after a model has been successfully created, saved, or updated.
* Argument: `inst`, model instance, object
*
* ```js
* MyModel.on('changed', function(inst) {
* console.log('model with id %s has been changed', inst.id);
* // => model with id 1 has been changed
* });
* ```
*
* #### Event: `deleted`
*
* Emitted after an individual model has been deleted.
* Argument: `id`, model ID (number).
*
* ```js
* MyModel.on('deleted', function(id) {
* console.log('model with id %s has been deleted', id);
* // => model with id 1 has been deleted
* });
* ```
*
* #### Event: `deletedAll`
*
* Emitted after all models have been deleted.
* Argument: `where` (optional), where filter, JSON object.
*
* ```js
* MyModel.on('deletedAll', function(where) {
* if (where) {
* console.log('all models where ', where, ' have been deleted');
* // => all models where
* // => {price: {gt: 100}}
* // => have been deleted
* }
* });
* ```
*
* #### Event: `attached`
*
* Emitted after a `Model` has been attached to an `app`.
*
* #### Event: `dataSourceAttached`
*
* Emitted after a `Model` has been attached to a `DataSource`.
*
* #### Event: set
*
* Emitted when model property is set.
* Argument: `inst`, model instance, object
*
* ```js
* MyModel.on('set', function(inst) {
* console.log('model with id %s has been changed', inst.id);
* // => model with id 1 has been changed
* });
* ```
*
* @param {Object} data
* @property {String} Model.modelName The name of the model. Static property.
* @property {DataSource} Model.dataSource Data source to which the model is connected, if any. Static property.
* @property {SharedClass} Model.sharedMethod The `strong-remoting` [SharedClass](http://apidocs.strongloop.com/strong-remoting/#sharedclass) that contains remoting (and http) metadata. Static property.
* @property {Object} settings Contains additional model settings.
* @property {string} settings.http.path Base URL of the model HTTP route.
* @property [{string}] settings.acls Array of ACLs for the model.
* @class
*/
var Model = registry.modelBuilder.define('Model');
Model.registry = registry;
/**
* The `loopback.Model.extend()` method calls this when you create a model that extends another model.
* Add any setup or configuration code you want executed when the model is created.
* See [Setting up a custom model](http://loopback.io/doc/en/lb2/Extending-built-in-models.html#setting-up-a-custom-model).
*/
Model.setup = function() {
var ModelCtor = this;
var Parent = this.super_;
Iif (!ModelCtor.registry && Parent && Parent.registry) {
ModelCtor.registry = Parent.registry;
}
var options = this.settings;
var typeName = this.modelName;
// support remoting prototype methods
// it's important to setup this function *before* calling `new SharedClass`
// otherwise remoting metadata from our base model is picked up
ModelCtor.sharedCtor = function(data, id, options, fn) {
var ModelCtor = this;
var isRemoteInvocationWithOptions = typeof data !== 'object' &&
typeof id === 'object' &&
typeof options === 'function';
if (isRemoteInvocationWithOptions) {
// sharedCtor(id, options, fn)
fn = options;
options = id;
id = data;
data = null;
} else if (typeof data === 'function') {
// sharedCtor(fn)
fn = data;
data = null;
id = null;
options = null;
} else if (typeof id === 'function') {
// sharedCtor(data, fn)
// sharedCtor(id, fn)
fn = id;
options = null;
if (typeof data !== 'object') {
id = data;
data = null;
} else {
id = null;
}
}
if (id && data) {
var model = new ModelCtor(data);
model.id = id;
fn(null, model);
} else if (data) {
fn(null, new ModelCtor(data));
} else if (id) {
var filter = {};
ModelCtor.findById(id, filter, options, function(err, model) {
if (err) {
fn(err);
} else if (model) {
fn(null, model);
} else {
err = new Error(g.f('could not find a model with {{id}} %s', id));
err.statusCode = 404;
err.code = 'MODEL_NOT_FOUND';
fn(err);
}
});
} else {
fn(new Error(g.f('must specify an {{id}} or {{data}}')));
}
};
var idDesc = ModelCtor.modelName + ' id';
ModelCtor.sharedCtor.accepts = [
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
description: idDesc},
// {arg: 'instance', type: 'object', http: {source: 'body'}}
{arg: 'options', type: 'object', http: createOptionsViaModelMethod},
];
ModelCtor.sharedCtor.http = [
{path: '/:id'},
];
ModelCtor.sharedCtor.returns = {root: true};
var remotingOptions = {};
extend(remotingOptions, options.remoting || {});
// create a sharedClass
var sharedClass = ModelCtor.sharedClass = new SharedClass(
ModelCtor.modelName,
ModelCtor,
remotingOptions
);
// before remote hook
ModelCtor.beforeRemote = function(name, fn) {
var className = this.modelName;
this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes();
remotes.before(className + '.' + name, function(ctx, next) {
return fn(ctx, ctx.result, next);
});
});
};
// after remote hook
ModelCtor.afterRemote = function(name, fn) {
var className = this.modelName;
this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes();
remotes.after(className + '.' + name, function(ctx, next) {
return fn(ctx, ctx.result, next);
});
});
};
ModelCtor.afterRemoteError = function(name, fn) {
var className = this.modelName;
this._runWhenAttachedToApp(function(app) {
var remotes = app.remotes();
remotes.afterError(className + '.' + name, fn);
});
};
ModelCtor._runWhenAttachedToApp = function(fn) {
Iif (this.app) return fn(this.app);
var self = this;
self.once('attached', function() {
fn(self.app);
});
};
Iif ('injectOptionsFromRemoteContext' in options) {
console.warn(g.f(
'%s is using model setting %s which is no longer available.',
typeName, 'injectOptionsFromRemoteContext'));
console.warn(g.f(
'Please rework your app to use the offical solution for injecting ' +
'"options" argument from request context,\nsee %s',
'http://loopback.io/doc/en/lb3/Using-current-context.html'));
}
// resolve relation functions
sharedClass.resolve(function resolver(define) {
var relations = ModelCtor.relations || {};
var defineRaw = define;
define = function(name, options, fn) {
if (options.accepts) {
options = extend({}, options);
options.accepts = setupOptionsArgs(options.accepts);
}
defineRaw(name, options, fn);
};
// get the relations
for (var relationName in relations) {
var relation = relations[relationName];
if (relation.type === 'belongsTo') {
ModelCtor.belongsToRemoting(relationName, relation, define);
} else if (
relation.type === 'hasOne' ||
relation.type === 'embedsOne'
) {
ModelCtor.hasOneRemoting(relationName, relation, define);
} else if (
relation.type === 'hasMany' ||
relation.type === 'embedsMany' ||
relation.type === 'referencesMany') {
ModelCtor.hasManyRemoting(relationName, relation, define);
}
}
// handle scopes
var scopes = ModelCtor.scopes || {};
for (var scopeName in scopes) {
ModelCtor.scopeRemoting(scopeName, scopes[scopeName], define);
}
});
return ModelCtor;
};
/*!
* Get the reference to ACL in a lazy fashion to avoid race condition in require
*/
var _aclModel = null;
Model._ACL = function getACL(ACL) {
var registry = this.registry;
if (ACL !== undefined) {
// The function is used as a setter
_aclModel = ACL;
}
if (_aclModel) {
return _aclModel;
}
var aclModel = registry.getModel('ACL');
_aclModel = registry.getModelByType(aclModel);
return _aclModel;
};
/**
* Check if the given access token can invoke the specified method.
*
* @param {AccessToken} token The access token.
* @param {*} modelId The model ID.
* @param {SharedMethod} sharedMethod The method in question.
* @param {Object} ctx The remote invocation context.
* @callback {Function} callback The callback function.
* @param {String|Error} err The error object.
* @param {Boolean} allowed True if the request is allowed; false otherwise.
*/
Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) {
var ANONYMOUS = registry.getModel('AccessToken').ANONYMOUS;
token = token || ANONYMOUS;
var aclModel = Model._ACL();
ctx = ctx || {};
if (typeof ctx === 'function' && callback === undefined) {
callback = ctx;
ctx = {};
}
aclModel.checkAccessForContext({
accessToken: token,
model: this,
property: sharedMethod.name,
method: sharedMethod.name,
sharedMethod: sharedMethod,
modelId: modelId,
accessType: this._getAccessTypeForMethod(sharedMethod),
remotingContext: ctx,
}, function(err, accessRequest) {
if (err) return callback(err);
callback(null, accessRequest.isAllowed());
});
};
/*!
* Determine the access type for the given `RemoteMethod`.
*
* @api private
* @param {RemoteMethod} method
*/
Model._getAccessTypeForMethod = function(method) {
if (typeof method === 'string') {
method = {name: method};
}
assert(
typeof method === 'object',
'method is a required argument and must be a RemoteMethod object'
);
var ACL = Model._ACL();
// Check the explicit setting of accessType
if (method.accessType) {
assert(method.accessType === ACL.READ ||
method.accessType === ACL.REPLICATE ||
method.accessType === ACL.WRITE ||
method.accessType === ACL.EXECUTE, 'invalid accessType ' +
method.accessType +
'. It must be "READ", "REPLICATE", "WRITE", or "EXECUTE"');
return method.accessType;
}
// Default GET requests to READ
var verb = method.http && method.http.verb;
if (typeof verb === 'string') {
verb = verb.toUpperCase();
}
if (verb === 'GET' || verb === 'HEAD') {
return ACL.READ;
}
switch (method.name) {
case 'create':
return ACL.WRITE;
case 'updateOrCreate':
return ACL.WRITE;
case 'upsertWithWhere':
return ACL.WRITE;
case 'upsert':
return ACL.WRITE;
case 'exists':
return ACL.READ;
case 'findById':
return ACL.READ;
case 'find':
return ACL.READ;
case 'findOne':
return ACL.READ;
case 'destroyById':
return ACL.WRITE;
case 'deleteById':
return ACL.WRITE;
case 'removeById':
return ACL.WRITE;
case 'count':
return ACL.READ;
default:
return ACL.EXECUTE;
}
};
/**
* Get the `Application` object to which the Model is attached.
*
* @callback {Function} callback Callback function called with `(err, app)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Application} app Attached application object.
* @end
*/
Model.getApp = function(callback) {
var self = this;
self._runWhenAttachedToApp(function(app) {
assert(self.app);
assert.equal(app, self.app);
callback(null, app);
});
};
/**
* Enable remote invocation for the specified method.
* See [Remote methods](http://loopback.io/doc/en/lb2/Remote-methods.html) for more information.
*
* Static method example:
* ```js
* Model.myMethod();
* Model.remoteMethod('myMethod');
* ```
*
* @param {String} name The name of the method.
* @param {Object} options The remoting options.
* See [Remote methods - Options](http://loopback.io/doc/en/lb2/Remote-methods.html#options).
*/
Model.remoteMethod = function(name, options) {
if (options.isStatic === undefined) {
var m = name.match(/^prototype\.(.*)$/);
options.isStatic = !m;
name = options.isStatic ? name : m[1];
}
Eif (options.accepts) {
options = extend({}, options);
options.accepts = setupOptionsArgs(options.accepts);
}
this.sharedClass.defineMethod(name, options);
this.emit('remoteMethodAdded', this.sharedClass);
};
function setupOptionsArgs(accepts) {
if (!Array.isArray(accepts))
accepts = [accepts];
return accepts.map(function(arg) {
if (arg.http && arg.http === 'optionsFromRequest') {
// clone to preserve the input value
arg = extend({}, arg);
arg.http = createOptionsViaModelMethod;
}
return arg;
});
}
function createOptionsViaModelMethod(ctx) {
var EMPTY_OPTIONS = {};
var ModelCtor = ctx.method && ctx.method.ctor;
if (!ModelCtor)
return EMPTY_OPTIONS;
if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function')
return EMPTY_OPTIONS;
debug('createOptionsFromRemotingContext for %s', ctx.method.stringName);
return ModelCtor.createOptionsFromRemotingContext(ctx);
}
/**
* Disable remote invocation for the method with the given name.
*
* @param {String} name The name of the method.
* @param {Boolean} isStatic Is the method static (eg. `MyModel.myMethod`)? Pass
* `false` if the method defined on the prototype (eg.
* `MyModel.prototype.myMethod`).
*/
Model.disableRemoteMethod = function(name, isStatic) {
deprecated('Model.disableRemoteMethod is deprecated. ' +
'Use Model.disableRemoteMethodByName instead.');
var key = this.sharedClass.getKeyFromMethodNameAndTarget(name, isStatic);
this.sharedClass.disableMethodByName(key);
this.emit('remoteMethodDisabled', this.sharedClass, key);
};
/**
* Disable remote invocation for the method with the given name.
*
* @param {String} name The name of the method (include "prototype." if the method is defined on the prototype).
*
*/
Model.disableRemoteMethodByName = function(name) {
this.sharedClass.disableMethodByName(name);
this.emit('remoteMethodDisabled', this.sharedClass, name);
};
Model.belongsToRemoting = function(relationName, relation, define) {
var modelName = relation.modelTo && relation.modelTo.modelName;
modelName = modelName || 'PersistedModel';
var fn = this.prototype[relationName];
var pathName = (relation.options.http && relation.options.http.path) || relationName;
define('__get__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName},
accepts: [
{arg: 'refresh', type: 'boolean', http: {source: 'query'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
accessType: 'READ',
description: format('Fetches belongsTo relation %s.', relationName),
returns: {arg: relationName, type: modelName, root: true},
}, fn);
};
function convertNullToNotFoundError(toModelName, ctx, cb) {
if (ctx.result !== null) return cb();
var fk = ctx.getArgByName('fk');
var msg = g.f('Unknown "%s" id "%s".', toModelName, fk);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
cb(error);
}
Model.hasOneRemoting = function(relationName, relation, define) {
var pathName = (relation.options.http && relation.options.http.path) || relationName;
var toModelName = relation.modelTo.modelName;
define('__get__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName},
accepts: [
{arg: 'refresh', type: 'boolean', http: {source: 'query'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Fetches hasOne relation %s.', relationName),
accessType: 'READ',
returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)},
});
define('__create__' + relationName, {
isStatic: false,
http: {verb: 'post', path: '/' + pathName},
accepts: [
{
arg: 'data', type: 'object', model: toModelName,
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Creates a new instance in %s of this model.', relationName),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true},
});
define('__update__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName},
accepts: [
{
arg: 'data', type: 'object', model: toModelName,
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Update %s of this model.', relationName),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true},
});
define('__destroy__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName},
accepts: [
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Deletes %s of this model.', relationName),
accessType: 'WRITE',
});
};
Model.hasManyRemoting = function(relationName, relation, define) {
var pathName = (relation.options.http && relation.options.http.path) || relationName;
var toModelName = relation.modelTo.modelName;
var findByIdFunc = this.prototype['__findById__' + relationName];
define('__findById__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName + '/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
description: format('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Find a related item by id for %s.', relationName),
accessType: 'READ',
returns: {arg: 'result', type: toModelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)},
}, findByIdFunc);
var destroyByIdFunc = this.prototype['__destroyById__' + relationName];
define('__destroyById__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
description: format('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Delete a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: [],
}, destroyByIdFunc);
var updateByIdFunc = this.prototype['__updateById__' + relationName];
define('__updateById__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/:fk'},
accepts: [
{arg: 'fk', type: 'any',
description: format('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}},
{arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Update a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: {arg: 'result', type: toModelName, root: true},
}, updateByIdFunc);
if (relation.modelThrough || relation.type === 'referencesMany') {
var modelThrough = relation.modelThrough || relation.modelTo;
var accepts = [];
if (relation.type === 'hasMany' && relation.modelThrough) {
// Restrict: only hasManyThrough relation can have additional properties
accepts.push({
arg: 'data', type: 'object', model: modelThrough.modelName,
http: {source: 'body'},
});
}
var addFunc = this.prototype['__link__' + relationName];
define('__link__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/rel/:fk'},
accepts: [{arg: 'fk', type: 'any',
description: format('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}},
].concat(accepts).concat([
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
description: format('Add a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: {arg: relationName, type: modelThrough.modelName, root: true},
}, addFunc);
var removeFunc = this.prototype['__unlink__' + relationName];
define('__unlink__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
description: format('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Remove the %s relation to an item by id.', relationName),
accessType: 'WRITE',
returns: [],
}, removeFunc);
// FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD?
// true --> 200 and false --> 404?
var existsFunc = this.prototype['__exists__' + relationName];
define('__exists__' + relationName, {
isStatic: false,
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
description: format('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Check the existence of %s relation to an item by id.', relationName),
accessType: 'READ',
returns: {arg: 'exists', type: 'boolean', root: true},
rest: {
// After hook to map exists to 200/404 for HEAD
after: function(ctx, cb) {
if (ctx.result === false) {
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
cb(error);
} else {
cb();
}
},
},
}, existsFunc);
}
};
Model.scopeRemoting = function(scopeName, scope, define) {
var pathName =
(scope.options && scope.options.http && scope.options.http.path) || scopeName;
var isStatic = scope.isStatic;
var toModelName = scope.modelTo.modelName;
// https://github.com/strongloop/loopback/issues/811
// Check if the scope is for a hasMany relation
var relation = this.relations[scopeName];
if (relation && relation.modelTo) {
// For a relation with through model, the toModelName should be the one
// from the target model
toModelName = relation.modelTo.modelName;
}
define('__get__' + scopeName, {
isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName},
accepts: [
{arg: 'filter', type: 'object'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Queries %s of %s.', scopeName, this.modelName),
accessType: 'READ',
returns: {arg: scopeName, type: [toModelName], root: true},
});
define('__create__' + scopeName, {
isStatic: isStatic,
http: {verb: 'post', path: '/' + pathName},
accepts: [
{
arg: 'data',
type: 'object',
allowArray: true,
model: toModelName,
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Creates a new instance in %s of this model.', scopeName),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true},
});
define('__delete__' + scopeName, {
isStatic: isStatic,
http: {verb: 'delete', path: '/' + pathName},
accepts: [
{
arg: 'where', type: 'object',
// The "where" argument is not exposed in the REST API
// but we need to provide a value so that we can pass "options"
// as the third argument.
http: function(ctx) { return undefined; },
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Deletes all %s of this model.', scopeName),
accessType: 'WRITE',
});
define('__count__' + scopeName, {
isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName + '/count'},
accepts: [
{
arg: 'where', type: 'object',
description: 'Criteria to match model instances',
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: format('Counts %s of %s.', scopeName, this.modelName),
accessType: 'READ',
returns: {arg: 'count', type: 'number'},
});
};
/**
* Enabled deeply-nested queries of related models via REST API.
*
* @param {String} relationName Name of the nested relation.
* @options {Object} [options] It is optional. See below.
* @param {String} pathName The HTTP path (relative to the model) at which your remote method is exposed.
* @param {String} filterMethod The filter name.
* @param {String} paramName The argument name that the remote method accepts.
* @param {String} getterName The getter name.
* @param {Boolean} hooks Whether to inherit before/after hooks.
* @callback {Function} filterCallback The Optional filter function.
* @param {Object} SharedMethod object. See [here](https://apidocs.strongloop.com/strong-remoting/#sharedmethod).
* @param {Object} RelationDefinition object which includes relation `type`, `ModelConstructor` of `modelFrom`, `modelTo`, `keyFrom`, `keyTo` and more relation definitions.
*/
Model.nestRemoting = function(relationName, options, filterCallback) {
if (typeof options === 'function' && !filterCallback) {
filterCallback = options;
options = {};
}
options = options || {};
var regExp = /^__([^_]+)__([^_]+)$/;
var relation = this.relations[relationName];
if (relation && relation.modelTo && relation.modelTo.sharedClass) {
var self = this;
var sharedClass = this.sharedClass;
var sharedToClass = relation.modelTo.sharedClass;
var toModelName = relation.modelTo.modelName;
var pathName = options.pathName || relation.options.path || relationName;
var paramName = options.paramName || 'nk';
var http = [].concat(sharedToClass.http || [])[0];
var httpPath, acceptArgs;
if (relation.multiple) {
httpPath = pathName + '/:' + paramName;
acceptArgs = [
{
arg: paramName, type: 'any', http: {source: 'path'},
description: format('Foreign key for %s.', relation.name),
required: true,
},
];
} else {
httpPath = pathName;
acceptArgs = [];
}
if (httpPath[0] !== '/') {
httpPath = '/' + httpPath;
}
// A method should return the method name to use, if it is to be
// included as a nested method - a falsy return value will skip.
var filter = filterCallback || options.filterMethod || function(method, relation) {
var matches = method.name.match(regExp);
if (matches) {
return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
}
};
sharedToClass.methods().forEach(function(method) {
var methodName;
if (!method.isStatic && (methodName = filter(method, relation))) {
var prefix = relation.multiple ? '__findById__' : '__get__';
var getterName = options.getterName || (prefix + relationName);
var getterFn = relation.modelFrom.prototype[getterName];
if (typeof getterFn !== 'function') {
throw new Error(g.f('Invalid remote method: `%s`', getterName));
}
var nestedFn = relation.modelTo.prototype[method.name];
if (typeof nestedFn !== 'function') {
throw new Error(g.f('Invalid remote method: `%s`', method.name));
}
var opts = {};
opts.accepts = acceptArgs.concat(method.accepts || []);
opts.returns = [].concat(method.returns || []);
opts.description = method.description;
opts.accessType = method.accessType;
opts.rest = extend({}, method.rest || {});
opts.rest.delegateTo = method;
opts.http = [];
var routes = [].concat(method.http || []);
routes.forEach(function(route) {
if (route.path) {
var copy = extend({}, route);
copy.path = httpPath + route.path;
opts.http.push(copy);
}
});
if (relation.multiple) {
sharedClass.defineMethod(methodName, opts, function(fkId) {
var args = Array.prototype.slice.call(arguments, 1);
var last = args[args.length - 1];
var cb = typeof last === 'function' ? last : null;
this[getterName](fkId, function(err, inst) {
if (err && cb) return cb(err);
if (inst instanceof relation.modelTo) {
try {
nestedFn.apply(inst, args);
} catch (err) {
if (cb) return cb(err);
}
} else if (cb) {
cb(err, null);
}
});
}, method.isStatic);
} else {
sharedClass.defineMethod(methodName, opts, function() {
var args = Array.prototype.slice.call(arguments);
var last = args[args.length - 1];
var cb = typeof last === 'function' ? last : null;
this[getterName](function(err, inst) {
if (err && cb) return cb(err);
if (inst instanceof relation.modelTo) {
try {
nestedFn.apply(inst, args);
} catch (err) {
if (cb) return cb(err);
}
} else if (cb) {
cb(err, null);
}
});
}, method.isStatic);
}
}
});
this.emit('remoteMethodAdded', this.sharedClass);
if (options.hooks === false) return; // don't inherit before/after hooks
self.once('mounted', function(app, sc, remotes) {
var listenerTree = extend({}, remotes.listenerTree || {});
listenerTree.before = listenerTree.before || {};
listenerTree.after = listenerTree.after || {};
var beforeListeners = listenerTree.before[toModelName] || {};
var afterListeners = listenerTree.after[toModelName] || {};
sharedClass.methods().forEach(function(method) {
var delegateTo = method.rest && method.rest.delegateTo;
if (delegateTo && delegateTo.ctor == relation.modelTo) {
var before = method.isStatic ? beforeListeners : beforeListeners['prototype'];
var after = method.isStatic ? afterListeners : afterListeners['prototype'];
var m = method.isStatic ? method.name : 'prototype.' + method.name;
if (before && before[delegateTo.name]) {
self.beforeRemote(m, function(ctx, result, next) {
before[delegateTo.name]._listeners.call(null, ctx, next);
});
}
if (after && after[delegateTo.name]) {
self.afterRemote(m, function(ctx, result, next) {
after[delegateTo.name]._listeners.call(null, ctx, next);
});
}
}
});
});
} else {
var msg = g.f('Relation `%s` does not exist for model `%s`', relationName, this.modelName);
throw new Error(msg);
}
};
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
/**
* Create "options" value to use when invoking model methods
* via strong-remoting (e.g. REST).
*
* Example
*
* ```js
* MyModel.myMethod = function(options, cb) {
* // by default, options contains only one property "accessToken"
* var accessToken = options && options.accessToken;
* var userId = accessToken && accessToken.userId;
* var message = 'Hello ' + (userId ? 'user #' + userId : 'anonymous');
* cb(null, message);
* });
*
* MyModel.remoteMethod('myMethod', {
* accepts: {
* arg: 'options',
* type: 'object',
* // "optionsFromRequest" is a loopback-specific HTTP mapping that
* // calls Model's createOptionsFromRemotingContext
* // to build the argument value
* http: 'optionsFromRequest'
* },
* returns: {
* arg: 'message',
* type: 'string'
* }
* });
* ```
*
* @param {Object} ctx A strong-remoting Context instance
* @returns {Object} The value to pass to "options" argument.
*/
Model.createOptionsFromRemotingContext = function(ctx) {
return {
accessToken: ctx.req.accessToken,
};
};
// setup the initial model
Model.setup();
return Model;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 | 1 1 1 1 1 1 1 1 1 1 1 1 1 12 12 12 12 12 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 12 12 12 12 1 180 180 180 180 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module Dependencies.
*/
'use strict';
var g = require('./globalize');
var runtime = require('./runtime');
var assert = require('assert');
var async = require('async');
var deprecated = require('depd')('loopback');
var debug = require('debug')('loopback:persisted-model');
var PassThrough = require('stream').PassThrough;
var utils = require('./utils');
var REPLICATION_CHUNK_SIZE = -1;
module.exports = function(registry) {
var Model = registry.getModel('Model');
/**
* Extends Model with basic query and CRUD support.
*
* **Change Event**
*
* Listen for model changes using the `change` event.
*
* ```js
* MyPersistedModel.on('changed', function(obj) {
* console.log(obj) // => the changed model
* });
* ```
*
* @class PersistedModel
*/
var PersistedModel = Model.extend('PersistedModel');
/*!
* Setup the `PersistedModel` constructor.
*/
PersistedModel.setup = function setupPersistedModel() {
// call Model.setup first
Model.setup.call(this);
var PersistedModel = this;
// enable change tracking (usually for replication)
Iif (this.settings.trackChanges) {
PersistedModel._defineChangeModel();
PersistedModel.once('dataSourceAttached', function() {
PersistedModel.enableChangeTracking();
});
} else Iif (this.settings.enableRemoteReplication) {
PersistedModel._defineChangeModel();
}
PersistedModel.setupRemoting();
};
/*!
* Throw an error telling the user that the method is not available and why.
*/
function throwNotAttached(modelName, methodName) {
throw new Error(
g.f('Cannot call %s.%s().' +
' The %s method has not been setup.' +
' The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!',
modelName, methodName, methodName)
);
}
/*!
* Convert null callbacks to 404 error objects.
* @param {HttpContext} ctx
* @param {Function} cb
*/
function convertNullToNotFoundError(ctx, cb) {
if (ctx.result !== null) return cb();
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
cb(error);
}
/**
* Create new instance of Model, and save to database.
*
* @param {Object|Object[]} [data] Optional data argument. Can be either a single model instance or an array of instances.
*
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} models Model instances or null.
*/
PersistedModel.create = function(data, callback) {
throwNotAttached(this.modelName, 'create');
};
/**
* Update or insert a model instance
* @param {Object} data The model instance data to insert.
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} model Updated model instance.
*/
PersistedModel.upsert = PersistedModel.updateOrCreate = PersistedModel.patchOrCreate =
function upsert(data, callback) {
throwNotAttached(this.modelName, 'upsert');
};
/**
* Update or insert a model instance based on the search criteria.
* If there is a single instance retrieved, update the retrieved model.
* Creates a new model if no model instances were found.
* Returns an error if multiple instances are found.
* @param {Object} [where] `where` filter, like
* ```
* { key: val, key2: {gt: 'val2'}, ...}
* ```
* <br/>see
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
* @param {Object} data The model instance data to insert.
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} model Updated model instance.
*/
PersistedModel.upsertWithWhere =
PersistedModel.patchOrCreateWithWhere = function upsertWithWhere(where, data, callback) {
throwNotAttached(this.modelName, 'upsertWithWhere');
};
/**
* Replace or insert a model instance; replace existing record if one is found,
* such that parameter `data.id` matches `id` of model instance; otherwise,
* insert a new record.
* @param {Object} data The model instance data.
* @options {Object} [options] Options for replaceOrCreate
* @property {Boolean} validate Perform validation before saving. Default is true.
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} model Replaced model instance.
*/
PersistedModel.replaceOrCreate = function replaceOrCreate(data, callback) {
throwNotAttached(this.modelName, 'replaceOrCreate');
};
/**
* Finds one record matching the optional filter object. If not found, creates
* the object using the data provided as second argument. In this sense it is
* the same as `find`, but limited to one object. Returns an object, not
* collection. If you don't provide the filter object argument, it tries to
* locate an existing object that matches the `data` argument.
*
* @options {Object} [filter] Optional Filter object; see below.
* @property {String|Object|Array} fields Identify fields to include in return result.
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
* @property {String|Object|Array} include See PersistedModel.include documentation.
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
* @property {Number} limit Maximum number of instances to return.
* <br/>See [Limit filter](http://loopback.io/doc/en/lb2/Limit-filter.html).
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
* <br/>See [Order filter](http://loopback.io/doc/en/lb2/Order-filter.html).
* @property {Number} skip Number of results to skip.
* <br/>See [Skip filter](http://loopback.io/doc/en/lb2/Skip-filter.html).
* @property {Object} where Where clause, like
* ```
* {where: {key: val, key2: {gt: val2}, ...}}
* ```
* <br/>See
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-queries).
* @param {Object} data Data to insert if object matching the `where` filter is not found.
* @callback {Function} callback Callback function called with `cb(err, instance, created)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Model instance matching the `where` filter, if found.
* @param {Boolean} created True if the instance does not exist and gets created.
*/
PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
throwNotAttached(this.modelName, 'findOrCreate');
};
PersistedModel.findOrCreate._delegate = true;
/**
* Check whether a model instance exists in database.
*
* @param {id} id Identifier of object (primary key value).
*
* @callback {Function} callback Callback function called with `(err, exists)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Boolean} exists True if the instance with the specified ID exists; false otherwise.
*/
PersistedModel.exists = function exists(id, cb) {
throwNotAttached(this.modelName, 'exists');
};
/**
* Find object by ID with an optional filter for include/fields.
*
* @param {*} id Primary key value
* @options {Object} [filter] Optional Filter JSON object; see below.
* @property {String|Object|Array} fields Identify fields to include in return result.
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
* @property {String|Object|Array} include See PersistedModel.include documentation.
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Model instance matching the specified ID or null if no instance matches.
*/
PersistedModel.findById = function findById(id, filter, cb) {
throwNotAttached(this.modelName, 'findById');
};
/**
* Find all model instances that match `filter` specification.
* See [Querying models](http://loopback.io/doc/en/lb2/Querying-data.html).
*
* @options {Object} [filter] Optional Filter JSON object; see below.
* @property {String|Object|Array} fields Identify fields to include in return result.
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
* @property {String|Object|Array} include See PersistedModel.include documentation.
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
* @property {Number} limit Maximum number of instances to return.
* <br/>See [Limit filter](http://loopback.io/doc/en/lb2/Limit-filter.html).
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
* <br/>See [Order filter](http://loopback.io/doc/en/lb2/Order-filter.html).
* @property {Number} skip Number of results to skip.
* <br/>See [Skip filter](http://loopback.io/doc/en/lb2/Skip-filter.html).
* @property {Object} where Where clause, like
* ```
* { where: { key: val, key2: {gt: 'val2'}, ...} }
* ```
* <br/>See
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-queries).
*
* @callback {Function} callback Callback function called with `(err, returned-instances)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Array} models Model instances matching the filter, or null if none found.
*/
PersistedModel.find = function find(filter, cb) {
throwNotAttached(this.modelName, 'find');
};
/**
* Find one model instance that matches `filter` specification.
* Same as `find`, but limited to one result;
* Returns object, not collection.
*
* @options {Object} [filter] Optional Filter JSON object; see below.
* @property {String|Object|Array} fields Identify fields to include in return result.
* <br/>See [Fields filter](http://loopback.io/doc/en/lb2/Fields-filter.html).
* @property {String|Object|Array} include See PersistedModel.include documentation.
* <br/>See [Include filter](http://loopback.io/doc/en/lb2/Include-filter.html).
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
* <br/>See [Order filter](http://loopback.io/doc/en/lb2/Order-filter.html).
* @property {Number} skip Number of results to skip.
* <br/>See [Skip filter](http://loopback.io/doc/en/lb2/Skip-filter.html).
* @property {Object} where Where clause, like
* ```
* {where: { key: val, key2: {gt: 'val2'}, ...} }
* ```
* <br/>See
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-queries).
*
* @callback {Function} callback Callback function called with `(err, returned-instance)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Array} model First model instance that matches the filter or null if none found.
*/
PersistedModel.findOne = function findOne(filter, cb) {
throwNotAttached(this.modelName, 'findOne');
};
/**
* Destroy all model instances that match the optional `where` specification.
*
* @param {Object} [where] Optional where filter, like:
* ```
* {key: val, key2: {gt: 'val2'}, ...}
* ```
* <br/>See
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
*
* @callback {Function} callback Optional callback function called with `(err, info)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} info Additional information about the command outcome.
* @param {Number} info.count Number of instances (rows, documents) destroyed.
*/
PersistedModel.destroyAll = function destroyAll(where, cb) {
throwNotAttached(this.modelName, 'destroyAll');
};
/**
* Alias for `destroyAll`
*/
PersistedModel.remove = PersistedModel.destroyAll;
/**
* Alias for `destroyAll`
*/
PersistedModel.deleteAll = PersistedModel.destroyAll;
/**
* Update multiple instances that match the where clause.
*
* Example:
*
*```js
* Employee.updateAll({managerId: 'x001'}, {managerId: 'x002'}, function(err, info) {
* ...
* });
* ```
*
* @param {Object} [where] Optional `where` filter, like
* ```
* { key: val, key2: {gt: 'val2'}, ...}
* ```
* <br/>see
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
* @param {Object} data Object containing data to replace matching instances, if any.
*
* @callback {Function} callback Callback function called with `(err, info)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} info Additional information about the command outcome.
* @param {Number} info.count Number of instances (rows, documents) updated.
*
*/
PersistedModel.updateAll = function updateAll(where, data, cb) {
throwNotAttached(this.modelName, 'updateAll');
};
/**
* Alias for updateAll.
*/
PersistedModel.update = PersistedModel.updateAll;
/**
* Destroy model instance with the specified ID.
* @param {*} id The ID value of model instance to delete.
* @callback {Function} callback Callback function called with `(err)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
*/
PersistedModel.destroyById = function deleteById(id, cb) {
throwNotAttached(this.modelName, 'deleteById');
};
/**
* Alias for destroyById.
*/
PersistedModel.removeById = PersistedModel.destroyById;
/**
* Alias for destroyById.
*/
PersistedModel.deleteById = PersistedModel.destroyById;
/**
* Return the number of records that match the optional "where" filter.
* @param {Object} [where] Optional where filter, like
* ```
* { key: val, key2: {gt: 'val2'}, ...}
* ```
* <br/>See
* [Where filter](http://loopback.io/doc/en/lb2/Where-filter.html#where-clause-for-other-methods).
* @callback {Function} callback Callback function called with `(err, count)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Number} count Number of instances.
*/
PersistedModel.count = function(where, cb) {
throwNotAttached(this.modelName, 'count');
};
/**
* Save model instance. If the instance doesn't have an ID, then calls [create](#persistedmodelcreatedata-cb) instead.
* Triggers: validate, save, update, or create.
* @options {Object} [options] See below.
* @property {Boolean} validate Perform validation before saving. Default is true.
* @property {Boolean} throws If true, throw a validation error; WARNING: This can crash Node.
* If false, report the error via callback. Default is false.
* @callback {Function} callback Optional callback function called with `(err, obj)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Model instance saved or created.
*/
PersistedModel.prototype.save = function(options, callback) {
var Model = this.constructor;
if (typeof options == 'function') {
callback = options;
options = {};
}
callback = callback || function() {
};
options = options || {};
if (!('validate' in options)) {
options.validate = true;
}
if (!('throws' in options)) {
options.throws = false;
}
var inst = this;
var data = inst.toObject(true);
var id = this.getId();
if (!id) {
return Model.create(this, callback);
}
// validate first
if (!options.validate) {
return save();
}
inst.isValid(function(valid) {
if (valid) {
save();
} else {
var err = new Model.ValidationError(inst);
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, inst);
}
});
// then save
function save() {
inst.trigger('save', function(saveDone) {
inst.trigger('update', function(updateDone) {
Model.upsert(inst, function(err) {
inst._initProperties(data);
updateDone.call(inst, function() {
saveDone.call(inst, function() {
callback(err, inst);
});
});
});
}, data);
}, data);
}
};
/**
* Determine if the data model is new.
* @returns {Boolean} Returns true if the data model is new; false otherwise.
*/
PersistedModel.prototype.isNewRecord = function() {
throwNotAttached(this.constructor.modelName, 'isNewRecord');
};
/**
* Deletes the model from persistence.
* Triggers `destroy` hook (async) before and after destroying object.
* @param {Function} callback Callback function.
*/
PersistedModel.prototype.destroy = function(cb) {
throwNotAttached(this.constructor.modelName, 'destroy');
};
/**
* Alias for destroy.
* @header PersistedModel.remove
*/
PersistedModel.prototype.remove = PersistedModel.prototype.destroy;
/**
* Alias for destroy.
* @header PersistedModel.delete
*/
PersistedModel.prototype.delete = PersistedModel.prototype.destroy;
PersistedModel.prototype.destroy._delegate = true;
/**
* Update a single attribute.
* Equivalent to `updateAttributes({name: 'value'}, cb)`
*
* @param {String} name Name of property.
* @param {Mixed} value Value of property.
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Updated instance.
*/
PersistedModel.prototype.updateAttribute = function updateAttribute(name, value, callback) {
throwNotAttached(this.constructor.modelName, 'updateAttribute');
};
/**
* Update set of attributes. Performs validation before updating.
*
* Triggers: `validation`, `save` and `update` hooks
* @param {Object} data Data to update.
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Updated instance.
*/
PersistedModel.prototype.updateAttributes = PersistedModel.prototype.patchAttributes =
function updateAttributes(data, cb) {
throwNotAttached(this.modelName, 'updateAttributes');
};
/**
* Replace attributes for a model instance and persist it into the datasource.
* Performs validation before replacing.
*
* @param {Object} data Data to replace.
* @options {Object} [options] Options for replace
* @property {Boolean} validate Perform validation before saving. Default is true.
* @callback {Function} callback Callback function called with `(err, instance)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Replaced instance.
*/
PersistedModel.prototype.replaceAttributes = function replaceAttributes(data, cb) {
throwNotAttached(this.modelName, 'replaceAttributes');
};
/**
* Replace attributes for a model instance whose id is the first input
* argument and persist it into the datasource.
* Performs validation before replacing.
*
* @param {*} id The ID value of model instance to replace.
* @param {Object} data Data to replace.
* @options {Object} [options] Options for replace
* @property {Boolean} validate Perform validation before saving. Default is true.
* @callback {Function} callback Callback function called with `(err, instance)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Replaced instance.
*/
PersistedModel.replaceById = function replaceById(id, data, cb) {
throwNotAttached(this.modelName, 'replaceById');
};
/**
* Reload object from persistence. Requires `id` member of `object` to be able to call `find`.
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} instance Model instance.
*/
PersistedModel.prototype.reload = function reload(callback) {
throwNotAttached(this.constructor.modelName, 'reload');
};
/**
* Set the correct `id` property for the `PersistedModel`. Uses the `setId` method if the model is attached to
* connector that defines it. Otherwise, uses the default lookup.
* Override this method to handle complex IDs.
*
* @param {*} val The `id` value. Will be converted to the type that the `id` property specifies.
*/
PersistedModel.prototype.setId = function(val) {
var ds = this.getDataSource();
this[this.getIdName()] = val;
};
/**
* Get the `id` value for the `PersistedModel`.
*
* @returns {*} The `id` value
*/
PersistedModel.prototype.getId = function() {
var data = this.toObject();
if (!data) return;
return data[this.getIdName()];
};
/**
* Get the `id` property name of the constructor.
*
* @returns {String} The `id` property name
*/
PersistedModel.prototype.getIdName = function() {
return this.constructor.getIdName();
};
/**
* Get the `id` property name of the constructor.
*
* @returns {String} The `id` property name
*/
PersistedModel.getIdName = function() {
var Model = this;
var ds = Model.getDataSource();
if (ds.idName) {
return ds.idName(Model.modelName);
} else {
return 'id';
}
};
PersistedModel.setupRemoting = function() {
var PersistedModel = this;
var typeName = PersistedModel.modelName;
var options = PersistedModel.settings;
// This is just for LB 3.x
options.replaceOnPUT = options.replaceOnPUT !== false;
function setRemoting(scope, name, options) {
var fn = scope[name];
fn._delegate = true;
options.isStatic = scope === PersistedModel;
PersistedModel.remoteMethod(name, options);
}
setRemoting(PersistedModel, 'create', {
description: 'Create a new instance of the model and persist it into the data source.',
accessType: 'WRITE',
accepts: [
{
arg: 'data', type: 'object', model: typeName, allowArray: true,
description: 'Model instance data',
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'post', path: '/'},
});
var upsertOptions = {
aliases: ['upsert', 'updateOrCreate'],
description: 'Patch an existing model instance or insert a new one ' +
'into the data source.',
accessType: 'WRITE',
accepts: [
{
arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
description: 'Model instance data',
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: [{verb: 'patch', path: '/'}],
};
Iif (!options.replaceOnPUT) {
upsertOptions.http.unshift({verb: 'put', path: '/'});
}
setRemoting(PersistedModel, 'patchOrCreate', upsertOptions);
var replaceOrCreateOptions = {
description: 'Replace an existing model instance or insert a new one into the data source.',
accessType: 'WRITE',
accepts: [
{
arg: 'data', type: 'object', model: typeName,
http: {source: 'body'},
description: 'Model instance data',
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: [{verb: 'post', path: '/replaceOrCreate'}],
};
Eif (options.replaceOnPUT) {
replaceOrCreateOptions.http.push({verb: 'put', path: '/'});
}
setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions);
setRemoting(PersistedModel, 'upsertWithWhere', {
aliases: ['patchOrCreateWithWhere'],
description: 'Update an existing model instance or insert a new one into ' +
'the data source based on the where criteria.',
accessType: 'WRITE',
accepts: [
{arg: 'where', type: 'object', http: {source: 'query'},
description: 'Criteria to match model instances'},
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
description: 'An object of model property name/value pairs'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'post', path: '/upsertWithWhere'},
});
setRemoting(PersistedModel, 'exists', {
description: 'Check whether a model instance exists in the data source.',
accessType: 'READ',
accepts: [
{arg: 'id', type: 'any', description: 'Model id', required: true},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'exists', type: 'boolean'},
http: [
{verb: 'get', path: '/:id/exists'},
{verb: 'head', path: '/:id'},
],
rest: {
// After hook to map exists to 200/404 for HEAD
after: function(ctx, cb) {
if (ctx.req.method === 'GET') {
// For GET, return {exists: true|false} as is
return cb();
}
if (!ctx.result.exists) {
var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id');
var msg = 'Unknown "' + modelName + '" id "' + id + '".';
var error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
cb(error);
} else {
cb();
}
},
},
});
setRemoting(PersistedModel, 'findById', {
description: 'Find a model instance by {{id}} from the data source.',
accessType: 'READ',
accepts: [
{arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}},
{arg: 'filter', type: 'object',
description:
'Filter defining fields and include - must be a JSON-encoded string (' +
'{"something":"value"})'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/:id'},
rest: {after: convertNullToNotFoundError},
});
var replaceByIdOptions = {
description: 'Replace attributes for a model instance and persist it into the data source.',
accessType: 'WRITE',
accepts: [
{arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}},
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description:
'Model instance data'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: [{verb: 'post', path: '/:id/replace'}],
};
Eif (options.replaceOnPUT) {
replaceByIdOptions.http.push({verb: 'put', path: '/:id'});
}
setRemoting(PersistedModel, 'replaceById', replaceByIdOptions);
setRemoting(PersistedModel, 'find', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{arg: 'filter', type: 'object', description:
'Filter defining fields, where, include, order, offset, and limit - must be a ' +
'JSON-encoded string ({"something":"value"})'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: [typeName], root: true},
http: {verb: 'get', path: '/'},
});
setRemoting(PersistedModel, 'findOne', {
description: 'Find first instance of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{arg: 'filter', type: 'object', description:
'Filter defining fields, where, include, order, offset, and limit - must be a ' +
'JSON-encoded string ({"something":"value"})'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/findOne'},
rest: {after: convertNullToNotFoundError},
});
setRemoting(PersistedModel, 'destroyAll', {
description: 'Delete all matching records.',
accessType: 'WRITE',
accepts: [
{arg: 'where', type: 'object', description: 'filter.where object'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {
arg: 'count',
type: 'object',
description: 'The number of instances deleted',
root: true,
},
http: {verb: 'del', path: '/'},
shared: false,
});
setRemoting(PersistedModel, 'updateAll', {
aliases: ['update'],
description: 'Update instances of the model matched by {{where}} from the data source.',
accessType: 'WRITE',
accepts: [
{arg: 'where', type: 'object', http: {source: 'query'},
description: 'Criteria to match model instances'},
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
description: 'An object of model property name/value pairs'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {
arg: 'info',
description: 'Information related to the outcome of the operation',
type: {
count: {
type: 'number',
description: 'The number of instances updated',
},
},
root: true,
},
http: {verb: 'post', path: '/update'},
});
setRemoting(PersistedModel, 'deleteById', {
aliases: ['destroyById', 'removeById'],
description: 'Delete a model instance by {{id}} from the data source.',
accessType: 'WRITE',
accepts: [
{arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
http: {verb: 'del', path: '/:id'},
returns: {arg: 'count', type: 'object', root: true},
});
setRemoting(PersistedModel, 'count', {
description: 'Count instances of the model matched by where from the data source.',
accessType: 'READ',
accepts: [
{arg: 'where', type: 'object', description: 'Criteria to match model instances'},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'count', type: 'number'},
http: {verb: 'get', path: '/count'},
});
var updateAttributesOptions = {
aliases: ['updateAttributes'],
description: 'Patch attributes for a model instance and persist it into ' +
'the data source.',
accessType: 'WRITE',
accepts: [
{
arg: 'data', type: 'object', model: typeName,
http: {source: 'body'},
description: 'An object of model property name/value pairs',
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
returns: {arg: 'data', type: typeName, root: true},
http: [{verb: 'patch', path: '/'}],
};
setRemoting(PersistedModel.prototype, 'patchAttributes', updateAttributesOptions);
Iif (!options.replaceOnPUT) {
updateAttributesOptions.http.unshift({verb: 'put', path: '/'});
}
Iif (options.trackChanges || options.enableRemoteReplication) {
setRemoting(PersistedModel, 'diff', {
description: 'Get a set of deltas and conflicts since the given checkpoint.',
accessType: 'READ',
accepts: [
{arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'},
{arg: 'remoteChanges', type: 'array', description: 'an array of change objects',
http: {source: 'body'}},
],
returns: {arg: 'result', type: 'object', root: true},
http: {verb: 'post', path: '/diff'},
});
setRemoting(PersistedModel, 'changes', {
description: 'Get the changes to a model since a given checkpoint.' +
'Provide a filter object to reduce the number of results returned.',
accessType: 'READ',
accepts: [
{arg: 'since', type: 'number', description:
'Only return changes since this checkpoint'},
{arg: 'filter', type: 'object', description:
'Only include changes that match this filter'},
],
returns: {arg: 'changes', type: 'array', root: true},
http: {verb: 'get', path: '/changes'},
});
setRemoting(PersistedModel, 'checkpoint', {
description: 'Create a checkpoint.',
// The replication algorithm needs to create a source checkpoint,
// even though it is otherwise not making any source changes.
// We need to allow this method for users that don't have full
// WRITE permissions.
accessType: 'REPLICATE',
returns: {arg: 'checkpoint', type: 'object', root: true},
http: {verb: 'post', path: '/checkpoint'},
});
setRemoting(PersistedModel, 'currentCheckpoint', {
description: 'Get the current checkpoint.',
accessType: 'READ',
returns: {arg: 'checkpoint', type: 'object', root: true},
http: {verb: 'get', path: '/checkpoint'},
});
setRemoting(PersistedModel, 'createUpdates', {
description: 'Create an update list from a delta list.',
// This operation is read-only, it does not change any local data.
// It is called by the replication algorithm to compile a list
// of changes to apply on the target.
accessType: 'READ',
accepts: {arg: 'deltas', type: 'array', http: {source: 'body'}},
returns: {arg: 'updates', type: 'array', root: true},
http: {verb: 'post', path: '/create-updates'},
});
setRemoting(PersistedModel, 'bulkUpdate', {
description: 'Run multiple updates at once. Note: this is not atomic.',
accessType: 'WRITE',
accepts: {arg: 'updates', type: 'array'},
http: {verb: 'post', path: '/bulk-update'},
});
setRemoting(PersistedModel, 'findLastChange', {
description: 'Get the most recent change record for this instance.',
accessType: 'READ',
accepts: {
arg: 'id', type: 'any', required: true, http: {source: 'path'},
description: 'Model id',
},
returns: {arg: 'result', type: this.Change.modelName, root: true},
http: {verb: 'get', path: '/:id/changes/last'},
});
setRemoting(PersistedModel, 'updateLastChange', {
description:
'Update the properties of the most recent change record ' +
'kept for this instance.',
accessType: 'WRITE',
accepts: [
{
arg: 'id', type: 'any', required: true, http: {source: 'path'},
description: 'Model id',
},
{
arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
description: 'An object of Change property name/value pairs',
},
],
returns: {arg: 'result', type: this.Change.modelName, root: true},
http: {verb: 'put', path: '/:id/changes/last'},
});
}
setRemoting(PersistedModel, 'createChangeStream', {
description: 'Create a change stream.',
accessType: 'READ',
http: [
{verb: 'post', path: '/change-stream'},
{verb: 'get', path: '/change-stream'},
],
accepts: {
arg: 'options',
type: 'object',
},
returns: {
arg: 'changes',
type: 'ReadableStream',
json: true,
},
});
};
/**
* Get a set of deltas and conflicts since the given checkpoint.
*
* See [Change.diff()](#change-diff) for details.
*
* @param {Number} since Find deltas since this checkpoint.
* @param {Array} remoteChanges An array of change objects.
* @callback {Function} callback Callback function called with `(err, result)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Object} result Object with `deltas` and `conflicts` properties; see [Change.diff()](#change-diff) for details.
*/
PersistedModel.diff = function(since, remoteChanges, callback) {
var Change = this.getChangeModel();
Change.diff(this.modelName, since, remoteChanges, callback);
};
/**
* Get the changes to a model since the specified checkpoint. Provide a filter object
* to reduce the number of results returned.
* @param {Number} since Return only changes since this checkpoint.
* @param {Object} filter Include only changes that match this filter, the same as for [#persistedmodel-find](find()).
* @callback {Function} callback Callback function called with `(err, changes)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Array} changes An array of [Change](#change) objects.
*/
PersistedModel.changes = function(since, filter, callback) {
if (typeof since === 'function') {
filter = {};
callback = since;
since = -1;
}
if (typeof filter === 'function') {
callback = filter;
since = -1;
filter = {};
}
var idName = this.dataSource.idName(this.modelName);
var Change = this.getChangeModel();
var model = this;
const changeFilter = this.createChangeFilter(since, filter);
filter = filter || {};
filter.fields = {};
filter.where = filter.where || {};
filter.fields[idName] = true;
// TODO(ritch) this whole thing could be optimized a bit more
Change.find(changeFilter, function(err, changes) {
if (err) return callback(err);
if (!Array.isArray(changes) || changes.length === 0) return callback(null, []);
var ids = changes.map(function(change) {
return change.getModelId();
});
filter.where[idName] = {inq: ids};
model.find(filter, function(err, models) {
if (err) return callback(err);
var modelIds = models.map(function(m) {
return m[idName].toString();
});
callback(null, changes.filter(function(ch) {
if (ch.type() === Change.DELETE) return true;
return modelIds.indexOf(ch.modelId) > -1;
}));
});
});
};
/**
* Create a checkpoint.
*
* @param {Function} callback
*/
PersistedModel.checkpoint = function(cb) {
var Checkpoint = this.getChangeModel().getCheckpointModel();
Checkpoint.bumpLastSeq(cb);
};
/**
* Get the current checkpoint ID.
*
* @callback {Function} callback Callback function called with `(err, currentCheckpointId)` arguments. Required.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Number} currentCheckpointId Current checkpoint ID.
*/
PersistedModel.currentCheckpoint = function(cb) {
var Checkpoint = this.getChangeModel().getCheckpointModel();
Checkpoint.current(cb);
};
/**
* Replicate changes since the given checkpoint to the given target model.
*
* @param {Number} [since] Since this checkpoint
* @param {Model} targetModel Target this model class
* @param {Object} [options] An optional options object to pass to underlying data-access calls.
* @param {Object} [options.filter] Replicate models that match this filter
* @callback {Function} [callback] Callback function called with `(err, conflicts)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {Conflict[]} conflicts A list of changes that could not be replicated due to conflicts.
* @param {Object} checkpoints The new checkpoints to use as the "since"
* argument for the next replication.
*
* @promise
*/
PersistedModel.replicate = function(since, targetModel, options, callback) {
var lastArg = arguments[arguments.length - 1];
if (typeof lastArg === 'function' && arguments.length > 1) {
callback = lastArg;
}
if (typeof since === 'function' && since.modelName) {
targetModel = since;
since = -1;
}
if (typeof since !== 'object') {
since = {source: since, target: since};
}
if (typeof options === 'function') {
options = {};
}
options = options || {};
var sourceModel = this;
callback = callback || utils.createPromiseCallback();
debug('replicating %s since %s to %s since %s',
sourceModel.modelName,
since.source,
targetModel.modelName,
since.target);
if (options.filter) {
debug('\twith filter %j', options.filter);
}
// In order to avoid a race condition between the replication and
// other clients modifying the data, we must create the new target
// checkpoint as the first step of the replication process.
// As a side-effect of that, the replicated changes are associated
// with the new target checkpoint. This is actually desired behaviour,
// because that way clients replicating *from* the target model
// since the new checkpoint will pick these changes up.
// However, it increases the likelihood of (false) conflicts being detected.
// In order to prevent that, we run the replication multiple times,
// until no changes were replicated, but at most MAX_ATTEMPTS times
// to prevent starvation. In most cases, the second run will find no changes
// to replicate and we are done.
var MAX_ATTEMPTS = 3;
run(1, since);
return callback.promise;
function run(attempt, since) {
debug('\titeration #%s', attempt);
tryReplicate(sourceModel, targetModel, since, options, next);
function next(err, conflicts, cps, updates) {
var finished = err || conflicts.length ||
!updates || updates.length === 0 ||
attempt >= MAX_ATTEMPTS;
if (finished)
return callback(err, conflicts, cps);
run(attempt + 1, cps);
}
}
};
function tryReplicate(sourceModel, targetModel, since, options, callback) {
var Change = sourceModel.getChangeModel();
var TargetChange = targetModel.getChangeModel();
var changeTrackingEnabled = Change && TargetChange;
var replicationChunkSize = REPLICATION_CHUNK_SIZE;
if (sourceModel.settings && sourceModel.settings.replicationChunkSize) {
replicationChunkSize = sourceModel.settings.replicationChunkSize;
}
assert(
changeTrackingEnabled,
'You must enable change tracking before replicating'
);
var diff, updates, newSourceCp, newTargetCp;
var tasks = [
checkpoints,
getSourceChanges,
getDiffFromTarget,
createSourceUpdates,
bulkUpdate,
];
async.waterfall(tasks, done);
function getSourceChanges(cb) {
utils.downloadInChunks(
options.filter,
replicationChunkSize,
function(filter, pagingCallback) {
sourceModel.changes(since.source, filter, pagingCallback);
},
debug.enabled ? log : cb);
function log(err, result) {
if (err) return cb(err);
debug('\tusing source changes');
result.forEach(function(it) { debug('\t\t%j', it); });
cb(err, result);
}
}
function getDiffFromTarget(sourceChanges, cb) {
utils.uploadInChunks(
sourceChanges,
replicationChunkSize,
function(smallArray, chunkCallback) {
return targetModel.diff(since.target, smallArray, chunkCallback);
},
debug.enabled ? log : cb);
function log(err, result) {
if (err) return cb(err);
if (result.conflicts && result.conflicts.length) {
debug('\tdiff conflicts');
result.conflicts.forEach(function(d) { debug('\t\t%j', d); });
}
if (result.deltas && result.deltas.length) {
debug('\tdiff deltas');
result.deltas.forEach(function(it) { debug('\t\t%j', it); });
}
cb(err, result);
}
}
function createSourceUpdates(_diff, cb) {
diff = _diff;
diff.conflicts = diff.conflicts || [];
if (diff && diff.deltas && diff.deltas.length) {
debug('\tbuilding a list of updates');
utils.uploadInChunks(
diff.deltas,
replicationChunkSize,
function(smallArray, chunkCallback) {
return sourceModel.createUpdates(smallArray, chunkCallback);
},
cb);
} else {
// nothing to replicate
done();
}
}
function bulkUpdate(_updates, cb) {
debug('\tstarting bulk update');
updates = _updates;
utils.uploadInChunks(
updates,
replicationChunkSize,
function(smallArray, chunkCallback) {
return targetModel.bulkUpdate(smallArray, options, function(err) {
// bulk update is a special case where we want to process all chunks and aggregate all errors
chunkCallback(null, err);
});
},
function(notUsed, err) {
var conflicts = err && err.details && err.details.conflicts;
if (conflicts && err.statusCode == 409) {
diff.conflicts = conflicts;
// filter out updates that were not applied
updates = updates.filter(function(u) {
return conflicts
.filter(function(d) { return d.modelId === u.change.modelId; })
.length === 0;
});
return cb();
}
cb(err);
});
}
function checkpoints() {
var cb = arguments[arguments.length - 1];
sourceModel.checkpoint(function(err, source) {
if (err) return cb(err);
newSourceCp = source.seq;
targetModel.checkpoint(function(err, target) {
if (err) return cb(err);
newTargetCp = target.seq;
debug('\tcreated checkpoints');
debug('\t\t%s for source model %s', newSourceCp, sourceModel.modelName);
debug('\t\t%s for target model %s', newTargetCp, targetModel.modelName);
cb();
});
});
}
function done(err) {
if (err) return callback(err);
debug('\treplication finished');
debug('\t\t%s conflict(s) detected', diff.conflicts.length);
debug('\t\t%s change(s) applied', updates ? updates.length : 0);
debug('\t\tnew checkpoints: { source: %j, target: %j }',
newSourceCp, newTargetCp);
var conflicts = diff.conflicts.map(function(change) {
return new Change.Conflict(
change.modelId, sourceModel, targetModel
);
});
if (conflicts.length) {
sourceModel.emit('conflicts', conflicts);
}
if (callback) {
var newCheckpoints = {source: newSourceCp, target: newTargetCp};
callback(null, conflicts, newCheckpoints, updates);
}
}
}
/**
* Create an update list (for `Model.bulkUpdate()`) from a delta list
* (result of `Change.diff()`).
*
* @param {Array} deltas
* @param {Function} callback
*/
PersistedModel.createUpdates = function(deltas, cb) {
var Change = this.getChangeModel();
var updates = [];
var Model = this;
var tasks = [];
deltas.forEach(function(change) {
change = new Change(change);
var type = change.type();
var update = {type: type, change: change};
switch (type) {
case Change.CREATE:
case Change.UPDATE:
tasks.push(function(cb) {
Model.findById(change.modelId, function(err, inst) {
if (err) return cb(err);
if (!inst) {
return cb &&
cb(new Error(g.f('Missing data for change: %s', change.modelId)));
}
if (inst.toObject) {
update.data = inst.toObject();
} else {
update.data = inst;
}
updates.push(update);
cb();
});
});
break;
case Change.DELETE:
updates.push(update);
break;
}
});
async.parallel(tasks, function(err) {
if (err) return cb(err);
cb(null, updates);
});
};
/**
* Apply an update list.
*
* **Note: this is not atomic**
*
* @param {Array} updates An updates list, usually from [createUpdates()](#persistedmodel-createupdates).
* @param {Object} [options] An optional options object to pass to underlying data-access calls.
* @param {Function} callback Callback function.
*/
PersistedModel.bulkUpdate = function(updates, options, callback) {
var tasks = [];
var Model = this;
var Change = this.getChangeModel();
var conflicts = [];
var lastArg = arguments[arguments.length - 1];
if (typeof lastArg === 'function' && arguments.length > 1) {
callback = lastArg;
}
if (typeof options === 'function') {
options = {};
}
options = options || {};
buildLookupOfAffectedModelData(Model, updates, function(err, currentMap) {
if (err) return callback(err);
updates.forEach(function(update) {
var id = update.change.modelId;
var current = currentMap[id];
switch (update.type) {
case Change.UPDATE:
tasks.push(function(cb) {
applyUpdate(Model, id, current, update.data, update.change, conflicts, options, cb);
});
break;
case Change.CREATE:
tasks.push(function(cb) {
applyCreate(Model, id, current, update.data, update.change, conflicts, options, cb);
});
break;
case Change.DELETE:
tasks.push(function(cb) {
applyDelete(Model, id, current, update.change, conflicts, options, cb);
});
break;
}
});
async.parallel(tasks, function(err) {
if (err) return callback(err);
if (conflicts.length) {
err = new Error(g.f('Conflict'));
err.statusCode = 409;
err.details = {conflicts: conflicts};
return callback(err);
}
callback();
});
});
};
function buildLookupOfAffectedModelData(Model, updates, callback) {
var idName = Model.dataSource.idName(Model.modelName);
var affectedIds = updates.map(function(u) { return u.change.modelId; });
var whereAffected = {};
whereAffected[idName] = {inq: affectedIds};
Model.find({where: whereAffected}, function(err, affectedList) {
if (err) return callback(err);
var dataLookup = {};
affectedList.forEach(function(it) {
dataLookup[it[idName]] = it;
});
callback(null, dataLookup);
});
}
function applyUpdate(Model, id, current, data, change, conflicts, options, cb) {
var Change = Model.getChangeModel();
var rev = current ? Change.revisionForInst(current) : null;
if (rev !== change.prev) {
debug('Detected non-rectified change of %s %j',
Model.modelName, id);
debug('\tExpected revision: %s', change.rev);
debug('\tActual revision: %s', rev);
conflicts.push(change);
return Change.rectifyModelChanges(Model.modelName, [id], cb);
}
// TODO(bajtos) modify `data` so that it instructs
// the connector to remove any properties included in "inst"
// but not included in `data`
// See https://github.com/strongloop/loopback/issues/1215
Model.updateAll(current.toObject(), data, options, function(err, result) {
if (err) return cb(err);
var count = result && result.count;
switch (count) {
case 1:
// The happy path, exactly one record was updated
return cb();
case 0:
debug('UpdateAll detected non-rectified change of %s %j',
Model.modelName, id);
conflicts.push(change);
// NOTE(bajtos) updateAll triggers change rectification
// for all model instances, even when no records were updated,
// thus we don't need to rectify explicitly ourselves
return cb();
case undefined:
case null:
return cb(new Error(
g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' +
'the number of updated records.')));
default:
debug('%s.updateAll modified unexpected number of instances: %j',
Model.modelName, count);
return cb(new Error(
g.f('Bulk update failed, the connector has modified unexpected ' +
'number of records: %s', JSON.stringify(count))));
}
});
}
function applyCreate(Model, id, current, data, change, conflicts, options, cb) {
Model.create(data, options, function(createErr) {
if (!createErr) return cb();
// We don't have a reliable way how to detect the situation
// where he model was not create because of a duplicate id
// The workaround is to query the DB to check if the model already exists
Model.findById(id, function(findErr, inst) {
if (findErr || !inst) {
// There isn't any instance with the same id, thus there isn't
// any conflict and we just report back the original error.
return cb(createErr);
}
return conflict();
});
});
function conflict() {
// The instance already exists - report a conflict
debug('Detected non-rectified new instance of %s %j',
Model.modelName, id);
conflicts.push(change);
var Change = Model.getChangeModel();
return Change.rectifyModelChanges(Model.modelName, [id], cb);
}
}
function applyDelete(Model, id, current, change, conflicts, options, cb) {
if (!current) {
// The instance was either already deleted or not created at all,
// we are done.
return cb();
}
var Change = Model.getChangeModel();
var rev = Change.revisionForInst(current);
if (rev !== change.prev) {
debug('Detected non-rectified change of %s %j',
Model.modelName, id);
debug('\tExpected revision: %s', change.rev);
debug('\tActual revision: %s', rev);
conflicts.push(change);
return Change.rectifyModelChanges(Model.modelName, [id], cb);
}
Model.deleteAll(current.toObject(), options, function(err, result) {
if (err) return cb(err);
var count = result && result.count;
switch (count) {
case 1:
// The happy path, exactly one record was updated
return cb();
case 0:
debug('DeleteAll detected non-rectified change of %s %j',
Model.modelName, id);
conflicts.push(change);
// NOTE(bajtos) deleteAll triggers change rectification
// for all model instances, even when no records were updated,
// thus we don't need to rectify explicitly ourselves
return cb();
case undefined:
case null:
return cb(new Error(
g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' +
'the number of deleted records.')));
default:
debug('%s.deleteAll modified unexpected number of instances: %j',
Model.modelName, count);
return cb(new Error(
g.f('Bulk update failed, the connector has deleted unexpected ' +
'number of records: %s', JSON.stringify(count))));
}
});
}
/**
* Get the `Change` model.
* Throws an error if the change model is not correctly setup.
* @return {Change}
*/
PersistedModel.getChangeModel = function() {
var changeModel = this.Change;
var isSetup = changeModel && changeModel.dataSource;
assert(isSetup, 'Cannot get a setup Change model for ' + this.modelName);
return changeModel;
};
/**
* Get the source identifier for this model or dataSource.
*
* @callback {Function} callback Callback function called with `(err, id)` arguments.
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
* @param {String} sourceId Source identifier for the model or dataSource.
*/
PersistedModel.getSourceId = function(cb) {
var dataSource = this.dataSource;
if (!dataSource) {
this.once('dataSourceAttached', this.getSourceId.bind(this, cb));
}
assert(
dataSource.connector.name,
'Model.getSourceId: cannot get id without dataSource.connector.name'
);
var id = [dataSource.connector.name, this.modelName].join('-');
cb(null, id);
};
/**
* Enable the tracking of changes made to the model. Usually for replication.
*/
PersistedModel.enableChangeTracking = function() {
var Model = this;
var Change = this.Change || this._defineChangeModel();
var cleanupInterval = Model.settings.changeCleanupInterval || 30000;
assert(this.dataSource, 'Cannot enableChangeTracking(): ' + this.modelName +
' is not attached to a dataSource');
var idName = this.getIdName();
var idProp = this.definition.properties[idName];
var idType = idProp && idProp.type;
var idDefn = idProp && idProp.defaultFn;
if (idType !== String || !(idDefn === 'uuid' || idDefn === 'guid')) {
deprecated('The model ' + this.modelName + ' is tracking changes, ' +
'which requires a string id with GUID/UUID default value.');
}
Model.observe('after save', rectifyOnSave);
Model.observe('after delete', rectifyOnDelete);
// Only run if the run time is server
// Can switch off cleanup by setting the interval to -1
if (runtime.isServer && cleanupInterval > 0) {
// initial cleanup
cleanup();
// cleanup
setInterval(cleanup, cleanupInterval);
}
function cleanup() {
Model.rectifyAllChanges(function(err) {
if (err) {
Model.handleChangeError(err, 'cleanup');
}
});
}
};
function rectifyOnSave(ctx, next) {
var instance = ctx.instance || ctx.currentInstance;
var id = instance ? instance.getId() :
getIdFromWhereByModelId(ctx.Model, ctx.where);
if (debug.enabled) {
debug('rectifyOnSave %s -> ' + (id ? 'id %j' : '%s'),
ctx.Model.modelName, id ? id : 'ALL');
debug('context instance:%j currentInstance:%j where:%j data %j',
ctx.instance, ctx.currentInstance, ctx.where, ctx.data);
}
if (id) {
ctx.Model.rectifyChange(id, reportErrorAndNext);
} else {
ctx.Model.rectifyAllChanges(reportErrorAndNext);
}
function reportErrorAndNext(err) {
if (err) {
ctx.Model.handleChangeError(err, 'after save');
}
next();
}
}
function rectifyOnDelete(ctx, next) {
var id = ctx.instance ? ctx.instance.getId() :
getIdFromWhereByModelId(ctx.Model, ctx.where);
if (debug.enabled) {
debug('rectifyOnDelete %s -> ' + (id ? 'id %j' : '%s'),
ctx.Model.modelName, id ? id : 'ALL');
debug('context instance:%j where:%j', ctx.instance, ctx.where);
}
if (id) {
ctx.Model.rectifyChange(id, reportErrorAndNext);
} else {
ctx.Model.rectifyAllChanges(reportErrorAndNext);
}
function reportErrorAndNext(err) {
if (err) {
ctx.Model.handleChangeError(err, 'after delete');
}
next();
}
}
function getIdFromWhereByModelId(Model, where) {
var idName = Model.getIdName();
if (!(idName in where)) return undefined;
var id = where[idName];
// TODO(bajtos) support object values that are not LB conditions
if (typeof id === 'string' || typeof id === 'number') {
return id;
}
return undefined;
}
PersistedModel._defineChangeModel = function() {
var BaseChangeModel = this.registry.getModel('Change');
assert(BaseChangeModel,
'Change model must be defined before enabling change replication');
const additionalChangeModelProperties =
this.settings.additionalChangeModelProperties || {};
this.Change = BaseChangeModel.extend(this.modelName + '-change',
additionalChangeModelProperties,
{trackModel: this}
);
if (this.dataSource) {
attachRelatedModels(this);
}
// Re-attach related models whenever our datasource is changed.
var self = this;
this.on('dataSourceAttached', function() {
attachRelatedModels(self);
});
return this.Change;
function attachRelatedModels(self) {
self.Change.attachTo(self.dataSource);
self.Change.getCheckpointModel().attachTo(self.dataSource);
}
};
PersistedModel.rectifyAllChanges = function(callback) {
this.getChangeModel().rectifyAll(callback);
};
/**
* Handle a change error. Override this method in a subclassing model to customize
* change error handling.
*
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb2/Error-object.html).
*/
PersistedModel.handleChangeError = function(err, operationName) {
if (!err) return;
this.emit('error', err, operationName);
};
/**
* Specify that a change to the model with the given ID has occurred.
*
* @param {*} id The ID of the model that has changed.
* @callback {Function} callback
* @param {Error} err
*/
PersistedModel.rectifyChange = function(id, callback) {
var Change = this.getChangeModel();
Change.rectifyModelChanges(this.modelName, [id], callback);
};
PersistedModel.findLastChange = function(id, cb) {
var Change = this.getChangeModel();
Change.findOne({where: {modelId: id}}, cb);
};
PersistedModel.updateLastChange = function(id, data, cb) {
var self = this;
this.findLastChange(id, function(err, inst) {
if (err) return cb(err);
if (!inst) {
err = new Error(g.f('No change record found for %s with id %s',
self.modelName, id));
err.statusCode = 404;
return cb(err);
}
inst.updateAttributes(data, cb);
});
};
/**
* Create a change stream. [See here for more info](http://loopback.io/doc/en/lb2/Realtime-server-sent-events.html)
*
* @param {Object} options
* @param {Object} options.where Only changes to models matching this where filter will be included in the `ChangeStream`.
* @callback {Function} callback
* @param {Error} err
* @param {ChangeStream} changes
*/
PersistedModel.createChangeStream = function(options, cb) {
if (typeof options === 'function') {
cb = options;
options = undefined;
}
var idName = this.getIdName();
var Model = this;
var changes = new PassThrough({objectMode: true});
var writeable = true;
changes.destroy = function() {
changes.removeAllListeners('error');
changes.removeAllListeners('end');
writeable = false;
changes = null;
};
changes.on('error', function() {
writeable = false;
});
changes.on('end', function() {
writeable = false;
});
process.nextTick(function() {
cb(null, changes);
});
Model.observe('after save', createChangeHandler('save'));
Model.observe('after delete', createChangeHandler('delete'));
function createChangeHandler(type) {
return function(ctx, next) {
// since it might have set to null via destroy
if (!changes) {
return next();
}
var where = ctx.where;
var data = ctx.instance || ctx.data;
var whereId = where && where[idName];
// the data includes the id
// or the where includes the id
var target;
if (data && (data[idName] || data[idName] === 0)) {
target = data[idName];
} else if (where && (where[idName] || where[idName] === 0)) {
target = where[idName];
}
var hasTarget = target === 0 || !!target;
var change = {
target: target,
where: where,
data: data,
};
switch (type) {
case 'save':
if (ctx.isNewInstance === undefined) {
change.type = hasTarget ? 'update' : 'create';
} else {
change.type = ctx.isNewInstance ? 'create' : 'update';
}
break;
case 'delete':
change.type = 'remove';
break;
}
// TODO(ritch) this is ugly... maybe a ReadableStream would be better
if (writeable) {
changes.write(change);
}
next();
};
}
};
/**
* Get the filter for searching related changes.
*
* Models should override this function to copy properties
* from the model instance filter into the change search filter.
*
* ```js
* module.exports = (TargetModel, config) => {
* TargetModel.createChangeFilter = function(since, modelFilter) {
* const filter = this.base.createChangeFilter.apply(this, arguments);
* if (modelFilter && modelFilter.where && modelFilter.where.tenantId) {
* filter.where.tenantId = modelFilter.where.tenantId;
* }
* return filter;
* };
* };
* ```
*
* @param {Number} since Return only changes since this checkpoint.
* @param {Object} modelFilter Filter describing which model instances to
* include in the list of changes.
* @returns {Object} The filter object to pass to `Change.find()`. Default:
* ```
* {where: {checkpoint: {gte: since}, modelName: this.modelName}}
* ```
*/
PersistedModel.createChangeFilter = function(since, modelFilter) {
return {
where: {
checkpoint: {gte: since},
modelName: this.modelName,
},
};
};
/**
* Add custom data to the Change instance.
*
* Models should override this function to duplicate model instance properties
* to the Change instance properties, typically to allow the changes() method
* to filter the changes using these duplicated properties directly while
* querying the Change model.
*
* ```js
* module.exports = (TargetModel, config) => {
* TargetModel.prototype.fillCustomChangeProperties = function(change, cb) {
* var inst = this;
* const base = this.constructor.base;
* base.prototype.fillCustomChangeProperties.call(this, change, err => {
* if (err) return cb(err);
*
* if (inst && inst.tenantId) {
* change.tenantId = inst.tenantId;
* } else {
* change.tenantId = null;
* }
*
* cb();
* });
* };
* };
* ```
*
* @callback {Function} callback
* @param {Error} err Error object; see [Error object](http://loopback.io/doc/en/lb3/Error-object.html).
*/
PersistedModel.prototype.fillCustomChangeProperties = function(change, cb) {
// no-op by default
cb();
};
PersistedModel.setup();
return PersistedModel;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 11 11 11 11 11 11 11 11 11 2 2 2 11 11 11 11 11 1 11 11 34 22 12 12 11 1 1 1 11 1 15 15 1 13 13 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('./globalize');
var assert = require('assert');
var extend = require('util')._extend;
var juggler = require('loopback-datasource-juggler');
var debug = require('debug')('loopback:registry');
var DataSource = juggler.DataSource;
var ModelBuilder = juggler.ModelBuilder;
var deprecated = require('depd')('strong-remoting');
module.exports = Registry;
/**
* Define and reference `Models` and `DataSources`.
*
* @class
*/
function Registry() {
this.defaultDataSources = {};
this.modelBuilder = new ModelBuilder();
require('./model')(this);
require('./persisted-model')(this);
// Set the default model base class.
this.modelBuilder.defaultModelBaseClass = this.getModel('Model');
}
/**
* Create a named vanilla JavaScript class constructor with an attached
* set of properties and options.
*
* This function comes with two variants:
* * `loopback.createModel(name, properties, options)`
* * `loopback.createModel(config)`
*
* In the second variant, the parameters `name`, `properties` and `options`
* are provided in the config object. Any additional config entries are
* interpreted as `options`, i.e. the following two configs are identical:
*
* ```js
* { name: 'Customer', base: 'User' }
* { name: 'Customer', options: { base: 'User' } }
* ```
*
* **Example**
*
* Create an `Author` model using the three-parameter variant:
*
* ```js
* loopback.createModel(
* 'Author',
* {
* firstName: 'string',
* lastName: 'string'
* },
* {
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* }
* );
* ```
*
* Create the same model using a config object:
*
* ```js
* loopback.createModel({
* name: 'Author',
* properties: {
* firstName: 'string',
* lastName: 'string'
* },
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* });
* ```
*
* @param {String} name Unique name.
* @param {Object} properties
* @param {Object} options (optional)
*
* @header loopback.createModel
*/
Registry.prototype.createModel = function(name, properties, options) {
Eif (arguments.length === 1 && typeof name === 'object') {
var config = name;
name = config.name;
properties = config.properties;
options = buildModelOptionsFromConfig(config);
assert(typeof name === 'string',
'The model-config property `name` must be a string');
}
options = options || {};
var BaseModel = options.base || options.super;
if (typeof BaseModel === 'string') {
var baseName = BaseModel;
BaseModel = this.findModel(BaseModel);
Iif (!BaseModel) {
throw new Error(g.f('Model not found: model `%s` is extending an unknown model `%s`.',
name, baseName));
}
}
BaseModel = BaseModel || this.getModel('PersistedModel');
var model = BaseModel.extend(name, properties, options);
model.registry = this;
this._defineRemoteMethods(model, model.settings.methods);
return model;
};
function buildModelOptionsFromConfig(config) {
var options = extend({}, config.options);
for (var key in config) {
if (['name', 'properties', 'options'].indexOf(key) !== -1) {
// Skip items which have special meaning
continue;
}
Iif (options[key] !== undefined) {
// When both `config.key` and `config.options.key` are set,
// use the latter one
continue;
}
options[key] = config[key];
}
return options;
}
/*
* Add the acl entry to the acls
* @param {Object[]} acls
* @param {Object} acl
*/
function addACL(acls, acl) {
for (var i = 0, n = acls.length; i < n; i++) {
// Check if there is a matching acl to be overriden
if (acls[i].property === acl.property &&
acls[i].accessType === acl.accessType &&
acls[i].principalType === acl.principalType &&
acls[i].principalId === acl.principalId) {
acls[i] = acl;
return;
}
}
acls.push(acl);
}
/**
* Alter an existing Model class.
* @param {Model} ModelCtor The model constructor to alter.
* @options {Object} config Additional configuration to apply
* @property {DataSource} dataSource Attach the model to a dataSource.
* @property {Object} [relations] Model relations to add/update.
*
* @header loopback.configureModel(ModelCtor, config)
*/
Registry.prototype.configureModel = function(ModelCtor, config) {
var settings = ModelCtor.settings;
var modelName = ModelCtor.modelName;
// Relations
if (typeof config.relations === 'object' && config.relations !== null) {
var relations = settings.relations = settings.relations || {};
Object.keys(config.relations).forEach(function(key) {
// FIXME: [rfeng] We probably should check if the relation exists
relations[key] = extend(relations[key] || {}, config.relations[key]);
});
} else if (config.relations != null) {
g.warn('The relations property of `%s` configuration ' +
'must be an object', modelName);
}
// ACLs
if (Array.isArray(config.acls)) {
var acls = settings.acls = settings.acls || [];
config.acls.forEach(function(acl) {
addACL(acls, acl);
});
} else if (config.acls != null) {
g.warn('The acls property of `%s` configuration ' +
'must be an array of objects', modelName);
}
// Settings
var excludedProperties = {
base: true,
'super': true,
relations: true,
acls: true,
dataSource: true,
};
if (typeof config.options === 'object' && config.options !== null) {
for (var p in config.options) {
if (!(p in excludedProperties)) {
settings[p] = config.options[p];
} else {
g.warn('Property `%s` cannot be reconfigured for `%s`',
p, modelName);
}
}
} else if (config.options != null) {
g.warn('The options property of `%s` configuration ' +
'must be an object', modelName);
}
// It's important to attach the datasource after we have updated
// configuration, so that the datasource picks up updated relations
if (config.dataSource) {
assert(config.dataSource instanceof DataSource,
'Cannot configure ' + ModelCtor.modelName +
': config.dataSource must be an instance of DataSource');
ModelCtor.attachTo(config.dataSource);
debug('Attached model `%s` to dataSource `%s`',
modelName, config.dataSource.name);
} else if (config.dataSource === null || config.dataSource === false) {
debug('Model `%s` is not attached to any DataSource by configuration.',
modelName);
} else {
debug('Model `%s` is not attached to any DataSource, possibly by a mistake.',
modelName);
g.warn(
'The configuration of `%s` is missing {{`dataSource`}} property.\n' +
'Use `null` or `false` to mark models not attached to any data source.',
modelName);
}
var newMethodNames = config.methods && Object.keys(config.methods);
var hasNewMethods = newMethodNames && newMethodNames.length;
var hasDescendants = this.getModelByType(ModelCtor) !== ModelCtor;
if (hasNewMethods && hasDescendants) {
g.warn(
'Child models of `%s` will not inherit newly defined remote methods %s.',
modelName, newMethodNames);
}
// Remote methods
this._defineRemoteMethods(ModelCtor, config.methods);
};
Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
Eif (!methods) return;
if (typeof methods !== 'object') {
g.warn('Ignoring non-object "methods" setting of "%s".',
ModelCtor.modelName);
return;
}
Object.keys(methods).forEach(function(key) {
var meta = methods[key];
var m = key.match(/^prototype\.(.*)$/);
var isStatic = !m;
if (typeof meta.isStatic !== 'boolean') {
key = isStatic ? key : m[1];
meta.isStatic = isStatic;
} else if (meta.isStatic && m) {
throw new Error(g.f('Remoting metadata for %s.%s {{"isStatic"}} does ' +
'not match new method name-based style.', ModelCtor.modelName, key));
} else {
key = isStatic ? key : m[1];
deprecated(g.f('Remoting metadata {{"isStatic"}} is deprecated. Please ' +
'specify {{"prototype.name"}} in method name instead for {{isStatic=false}}.'));
}
ModelCtor.remoteMethod(key, meta);
});
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`
* @param {String|Function} modelOrName The model name or a `Model` constructor.
* @returns {Model} The model class
*
* @header loopback.findModel(modelName)
*/
Registry.prototype.findModel = function(modelName) {
Iif (typeof modelName === 'function') return modelName;
return this.modelBuilder.models[modelName];
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`. **Throw an error when no such model exists.**
*
* @param {String} modelOrName The model name or a `Model` constructor.
* @returns {Model} The model class
*
* @header loopback.getModel(modelName)
*/
Registry.prototype.getModel = function(modelName) {
var model = this.findModel(modelName);
Eif (model) return model;
throw new Error(g.f('Model not found: %s', modelName));
};
/**
* Look up a model class by the base model class.
* The method can be used by LoopBack
* to find configured models in models.json over the base model.
* @param {Model} modelType The base model class
* @returns {Model} The subclass if found or the base class
*
* @header loopback.getModelByType(modelType)
*/
Registry.prototype.getModelByType = function(modelType) {
var type = typeof modelType;
var accepted = ['function', 'string'];
assert(accepted.indexOf(type) > -1,
'The model type must be a constructor or model name');
if (type === 'string') {
modelType = this.getModel(modelType);
}
var models = this.modelBuilder.models;
for (var m in models) {
if (models[m].prototype instanceof modelType) {
return models[m];
}
}
return modelType;
};
/**
* Create a data source with passing the provided options to the connector.
*
* @param {String} name Optional name.
* @options {Object} options Data Source options
* @property {Object} connector LoopBack connector.
* @property {*} [*] Other connector properties.
* See the relevant connector documentation.
*/
Registry.prototype.createDataSource = function(name, options) {
var self = this;
var ds = new DataSource(name, options, self.modelBuilder);
ds.createModel = function(name, properties, settings) {
settings = settings || {};
var BaseModel = settings.base || settings.super;
if (!BaseModel) {
// Check the connector types
var connectorTypes = ds.getTypes();
if (Array.isArray(connectorTypes) && connectorTypes.indexOf('db') !== -1) {
// Only set up the base model to PersistedModel if the connector is DB
BaseModel = self.PersistedModel;
} else {
BaseModel = self.Model;
}
settings.base = BaseModel;
}
var ModelCtor = self.createModel(name, properties, settings);
ModelCtor.attachTo(ds);
return ModelCtor;
};
if (ds.settings && ds.settings.defaultForType) {
var msg = g.f('{{DataSource}} option {{"defaultForType"}} is no longer supported');
throw new Error(msg);
}
return ds;
};
/**
* Get an in-memory data source. Use one if it already exists.
*
* @param {String} [name] The name of the data source.
* If not provided, the `'default'` is used.
*/
Registry.prototype.memory = function(name) {
name = name || 'default';
var memory = (
this._memoryDataSources || (this._memoryDataSources = {})
)[name];
if (!memory) {
memory = this._memoryDataSources[name] = this.createDataSource({
connector: 'memory',
});
}
return memory;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 1 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved. // Node module: loopback // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT /* * This is an internal file that should not be used outside of loopback. * All exported entities can be accessed via the `loopback` object. * @private */ 'use strict'; var runtime = exports; /** * True if running in a browser environment; false otherwise. * @header loopback.isBrowser */ runtime.isBrowser = typeof window !== 'undefined'; /** * True if running in a server environment; false otherwise. * @header loopback.isServer */ runtime.isServer = !runtime.isBrowser; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('./globalize');
var assert = require('assert');
var express = require('express');
var merge = require('util')._extend;
var mergePhaseNameLists = require('loopback-phase').mergePhaseNameLists;
var debug = require('debug')('loopback:app');
var stableSortInPlace = require('stable').inplace;
var BUILTIN_MIDDLEWARE = {builtin: true};
var proto = {};
module.exports = function loopbackExpress() {
var app = express();
app.__expressLazyRouter = app.lazyrouter;
merge(app, proto);
return app;
};
/**
* Register a middleware using a factory function and a JSON config.
*
* **Example**
*
* ```js
* app.middlewareFromConfig(compression, {
* enabled: true,
* phase: 'initial',
* params: {
* threshold: 128
* }
* });
* ```
*
* @param {function} factory The factory function creating a middleware handler.
* Typically a result of `require()` call, e.g. `require('compression')`.
* @options {Object} config The configuration.
* @property {String} phase The phase to register the middleware in.
* @property {Boolean} [enabled] Whether the middleware is enabled.
* Default: `true`.
* @property {Array|*} [params] The arguments to pass to the factory
* function. Either an array of arguments,
* or the value of the first argument when the factory expects
* a single argument only.
* @property {Array|string|RegExp} [paths] Optional list of paths limiting
* the scope of the middleware.
*
* @returns {object} this (fluent API)
*
* @header app.middlewareFromConfig(factory, config)
*/
proto.middlewareFromConfig = function(factory, config) {
assert(typeof factory === 'function', '"factory" must be a function');
assert(typeof config === 'object', '"config" must be an object');
assert(typeof config.phase === 'string' && config.phase,
'"config.phase" must be a non-empty string');
if (config.enabled === false)
return;
var params = config.params;
if (params === undefined) {
params = [];
} else if (!Array.isArray(params)) {
params = [params];
}
var handler = factory.apply(null, params);
// Check if methods/verbs filter exists
var verbs = config.methods || config.verbs;
if (Array.isArray(verbs)) {
verbs = verbs.map(function(verb) {
return verb && verb.toUpperCase();
});
if (verbs.indexOf('ALL') === -1) {
var originalHandler = handler;
if (handler.length <= 3) {
// Regular handler
handler = function(req, res, next) {
if (verbs.indexOf(req.method.toUpperCase()) === -1) {
return next();
}
originalHandler(req, res, next);
};
} else {
// Error handler
handler = function(err, req, res, next) {
if (verbs.indexOf(req.method.toUpperCase()) === -1) {
return next(err);
}
originalHandler(err, req, res, next);
};
}
}
}
this.middleware(config.phase, config.paths || [], handler);
return this;
};
/**
* Register (new) middleware phases.
*
* If all names are new, then the phases are added just before "routes" phase.
* Otherwise the provided list of names is merged with the existing phases
* in such way that the order of phases is preserved.
*
* **Examples**
*
* ```js
* // built-in phases:
* // initial, session, auth, parse, routes, files, final
*
* app.defineMiddlewarePhases('custom');
* // new list of phases
* // initial, session, auth, parse, custom, routes, files, final
*
* app.defineMiddlewarePhases([
* 'initial', 'postinit', 'preauth', 'routes', 'subapps'
* ]);
* // new list of phases
* // initial, postinit, preauth, session, auth, parse, custom,
* // routes, subapps, files, final
* ```
*
* @param {string|Array.<string>} nameOrArray A phase name or a list of phase
* names to add.
*
* @returns {object} this (fluent API)
*
* @header app.defineMiddlewarePhases(nameOrArray)
*/
proto.defineMiddlewarePhases = function(nameOrArray) {
this.lazyrouter();
if (Array.isArray(nameOrArray)) {
this._requestHandlingPhases =
mergePhaseNameLists(this._requestHandlingPhases, nameOrArray);
} else {
// add the new phase before 'routes'
var routesIx = this._requestHandlingPhases.indexOf('routes');
this._requestHandlingPhases.splice(routesIx - 1, 0, nameOrArray);
}
return this;
};
/**
* Register a middleware handler to be executed in a given phase.
* @param {string} name The phase name, e.g. "init" or "routes".
* @param {Array|string|RegExp} [paths] Optional list of paths limiting
* the scope of the middleware.
* String paths are interpreted as expressjs path patterns,
* regular expressions are used as-is.
* @param {function} handler The middleware handler, one of
* `function(req, res, next)` or
* `function(err, req, res, next)`
* @returns {object} this (fluent API)
*
* @header app.middleware(name, handler)
*/
proto.middleware = function(name, paths, handler) {
this.lazyrouter();
if (handler === undefined && typeof paths === 'function') {
handler = paths;
paths = undefined;
}
assert(typeof name === 'string' && name, '"name" must be a non-empty string');
assert(typeof handler === 'function', '"handler" must be a function');
if (paths === undefined) {
paths = '/';
}
var fullPhaseName = name;
var handlerName = handler.name || '<anonymous>';
var m = name.match(/^(.+):(before|after)$/);
if (m) {
name = m[1];
}
if (this._requestHandlingPhases.indexOf(name) === -1)
throw new Error(g.f('Unknown {{middleware}} phase %s', name));
debug('use %s %s %s', fullPhaseName, paths, handlerName);
this._skipLayerSorting = true;
this.use(paths, handler);
var layer = this._findLayerByHandler(handler);
if (layer) {
// Set the phase name for sorting
layer.phase = fullPhaseName;
} else {
debug('No matching layer is found for %s %s', fullPhaseName, handlerName);
}
this._skipLayerSorting = false;
this._sortLayersByPhase();
return this;
};
/*!
* Find the corresponding express layer by handler
*
* This is needed because monitoring agents such as NewRelic can add handlers
* to the stack. For example, NewRelic adds sentinel handler. We need to search
* the stackto find the correct layer.
*/
proto._findLayerByHandler = function(handler) {
// Other handlers can be added to the stack, for example,
// NewRelic adds sentinel handler. We need to search the stack
for (var k = this._router.stack.length - 1; k >= 0; k--) {
if (this._router.stack[k].handle === handler ||
// NewRelic replaces the handle and keeps it as __NR_original
this._router.stack[k].handle['__NR_original'] === handler
) {
return this._router.stack[k];
} else {
// Aggressively check if the original handler has been wrapped
// into a new function with a property pointing to the original handler
for (var p in this._router.stack[k].handle) {
if (this._router.stack[k].handle[p] === handler) {
return this._router.stack[k];
}
}
}
}
return null;
};
// Install our custom PhaseList-based handler into the app
proto.lazyrouter = function() {
var self = this;
if (self._router) return;
self.__expressLazyRouter();
var router = self._router;
// Mark all middleware added by Router ctor as builtin
// The sorting algo will keep them at beginning of the list
router.stack.forEach(function(layer) {
layer.phase = BUILTIN_MIDDLEWARE;
});
router.__expressUse = router.use;
router.use = function useAndSort() {
var retval = this.__expressUse.apply(this, arguments);
self._sortLayersByPhase();
return retval;
};
router.__expressRoute = router.route;
router.route = function routeAndSort() {
var retval = this.__expressRoute.apply(this, arguments);
self._sortLayersByPhase();
return retval;
};
self._requestHandlingPhases = [
'initial', 'session', 'auth', 'parse',
'routes', 'files', 'final',
];
};
proto._sortLayersByPhase = function() {
if (this._skipLayerSorting) return;
var phaseOrder = {};
this._requestHandlingPhases.forEach(function(name, ix) {
phaseOrder[name + ':before'] = ix * 3;
phaseOrder[name] = ix * 3 + 1;
phaseOrder[name + ':after'] = ix * 3 + 2;
});
var router = this._router;
stableSortInPlace(router.stack, compareLayers);
function compareLayers(left, right) {
var leftPhase = left.phase;
var rightPhase = right.phase;
if (leftPhase === rightPhase) return 0;
// Builtin middleware is always first
if (leftPhase === BUILTIN_MIDDLEWARE) return -1;
if (rightPhase === BUILTIN_MIDDLEWARE) return 1;
// Layers registered via app.use and app.route
// are executed as the first items in `routes` phase
if (leftPhase === undefined) {
if (rightPhase === 'routes')
return -1;
return phaseOrder['routes'] - phaseOrder[rightPhase];
}
if (rightPhase === undefined)
return -compareLayers(right, left);
// Layers registered via `app.middleware` are compared via phase & hook
return phaseOrder[leftPhase] - phaseOrder[rightPhase];
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2015. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
exports.createPromiseCallback = createPromiseCallback;
exports.uploadInChunks = uploadInChunks;
exports.downloadInChunks = downloadInChunks;
exports.concatResults = concatResults;
var Promise = require('bluebird');
var async = require('async');
function createPromiseCallback() {
var cb;
var promise = new Promise(function(resolve, reject) {
cb = function(err, data) {
if (err) return reject(err);
return resolve(data);
};
});
cb.promise = promise;
return cb;
}
function throwPromiseNotDefined() {
throw new Error(
'Your Node runtime does support ES6 Promises. ' +
'Set "global.Promise" to your preferred implementation of promises.');
}
/**
* Divide an async call with large array into multiple calls using smaller chunks
* @param {Array} largeArray - the large array to be chunked
* @param {Number} chunkSize - size of each chunks
* @param {Function} processFunction - the function to be called multiple times
* @param {Function} cb - the callback
*/
function uploadInChunks(largeArray, chunkSize, processFunction, cb) {
var chunkArrays = [];
if (!chunkSize || chunkSize < 1 || largeArray.length <= chunkSize) {
// if chunking not required
processFunction(largeArray, cb);
} else {
// copying so that the largeArray object does not get affected during splice
var copyOfLargeArray = [].concat(largeArray);
// chunking to smaller arrays
while (copyOfLargeArray.length > 0) {
chunkArrays.push(copyOfLargeArray.splice(0, chunkSize));
}
var tasks = chunkArrays.map(function(chunkArray) {
return function(previousResults, chunkCallback) {
var lastArg = arguments[arguments.length - 1];
if (typeof lastArg === 'function') {
chunkCallback = lastArg;
}
processFunction(chunkArray, function(err, results) {
if (err) {
return chunkCallback(err);
}
// if this is the first async waterfall call or if previous results was not defined
if (typeof previousResults === 'function' || typeof previousResults === 'undefined' ||
previousResults === null) {
previousResults = results;
} else if (results) {
previousResults = concatResults(previousResults, results);
}
chunkCallback(err, previousResults);
});
};
});
async.waterfall(tasks, cb);
}
}
/**
* Page async download calls
* @param {Object} filter - filter object used for the async call
* @param {Number} chunkSize - size of each chunks
* @param {Function} processFunction - the function to be called multiple times
* @param {Function} cb - the callback
*/
function downloadInChunks(filter, chunkSize, processFunction, cb) {
var results = [];
filter = filter ? JSON.parse(JSON.stringify(filter)) : {};
if (!chunkSize || chunkSize < 1) {
// if chunking not required
processFunction(filter, cb);
} else {
filter.skip = 0;
filter.limit = chunkSize;
processFunction(JSON.parse(JSON.stringify(filter)), pageAndConcatResults);
}
function pageAndConcatResults(err, pagedResults) {
if (err) {
return cb(err);
} else {
results = concatResults(results, pagedResults);
if (pagedResults.length >= chunkSize) {
filter.skip += pagedResults.length;
processFunction(JSON.parse(JSON.stringify(filter)), pageAndConcatResults);
} else {
cb(null, results);
}
}
}
}
/**
* Concat current results into previous results
* Assumption made here that the previous results and current results are homogeneous
* @param {Object|Array} previousResults
* @param {Object|Array} currentResults
*/
function concatResults(previousResults, currentResults) {
if (Array.isArray(currentResults)) {
previousResults = previousResults.concat(currentResults);
} else if (typeof currentResults === 'object') {
Object.keys(currentResults).forEach(function(key) {
previousResults[key] = concatResults(previousResults[key], currentResults[key]);
});
} else {
previousResults = currentResults;
}
return previousResults;
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| base-connector.js | 66.67% | (10 / 15) | 100% | (0 / 0) | 0% | (0 / 4) | 66.67% | (10 / 15) | |
| mail.js | 23.88% | (16 / 67) | 0% | (0 / 35) | 0% | (0 / 9) | 23.88% | (16 / 67) | |
| memory.js | 100% | (10 / 10) | 100% | (0 / 0) | 0% | (0 / 1) | 100% | (10 / 10) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2014. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/**
* Expose `Connector`.
*/
'use strict';
module.exports = Connector;
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('connector');
var util = require('util');
var inherits = util.inherits;
var assert = require('assert');
/**
* Create a new `Connector` with the given `options`.
*
* @param {Object} options
* @return {Connector}
*/
function Connector(options) {
EventEmitter.apply(this, arguments);
this.options = options;
debug('created with options', options);
}
/**
* Inherit from `EventEmitter`.
*/
inherits(Connector, EventEmitter);
/*!
* Create an connector instance from a JugglingDB adapter.
*/
Connector._createJDBAdapter = function(jdbModule) {
var fauxSchema = {};
jdbModule.initialize(fauxSchema, function() {
// connected
});
};
/*!
* Add default crud operations from a JugglingDB adapter.
*/
Connector.prototype._addCrudOperationsFromJDBAdapter = function(connector) {
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/**
* Dependencies.
*/
'use strict';
var g = require('../globalize');
var mailer = require('nodemailer');
var assert = require('assert');
var debug = require('debug')('loopback:connector:mail');
var loopback = require('../loopback');
/**
* Export the MailConnector class.
*/
module.exports = MailConnector;
/**
* Create an instance of the connector with the given `settings`.
*/
function MailConnector(settings) {
assert(typeof settings === 'object', 'cannot initialize MailConnector without a settings object');
var transports = settings.transports;
// if transports is not in settings object AND settings.transport exists
if (!transports && settings.transport) {
// then wrap single transport in an array and assign to transports
transports = [settings.transport];
}
if (!transports) {
transports = [];
}
this.transportsIndex = {};
this.transports = [];
if (loopback.isServer) {
transports.forEach(this.setupTransport.bind(this));
}
}
MailConnector.initialize = function(dataSource, callback) {
dataSource.connector = new MailConnector(dataSource.settings);
callback();
};
MailConnector.prototype.DataAccessObject = Mailer;
/**
* Add a transport to the available transports. See https://github.com/andris9/Nodemailer#setting-up-a-transport-method.
*
* Example:
*
* Email.setupTransport({
* type: "SMTP",
* host: "smtp.gmail.com", // hostname
* secureConnection: true, // use SSL
* port: 465, // port for secure SMTP
* alias: "gmail", // optional alias for use with 'transport' option when sending
* auth: {
* user: "gmail.user@gmail.com",
* pass: "userpass"
* }
* });
*
*/
MailConnector.prototype.setupTransport = function(setting) {
var connector = this;
connector.transports = connector.transports || [];
connector.transportsIndex = connector.transportsIndex || {};
var transport;
var transportType = (setting.type || 'STUB').toLowerCase();
if (transportType === 'direct') {
transport = mailer.createTransport();
} else if (transportType === 'smtp') {
transport = mailer.createTransport(setting);
} else {
var transportModuleName = 'nodemailer-' + transportType + '-transport';
var transportModule = require(transportModuleName);
transport = mailer.createTransport(transportModule(setting));
}
connector.transportsIndex[setting.alias || setting.type] = transport;
connector.transports.push(transport);
};
function Mailer() {
}
/**
* Get a transport by name.
*
* @param {String} name
* @return {Transport} transport
*/
MailConnector.prototype.transportForName = function(name) {
return this.transportsIndex[name];
};
/**
* Get the default transport.
*
* @return {Transport} transport
*/
MailConnector.prototype.defaultTransport = function() {
return this.transports[0] || this.stubTransport;
};
/**
* Send an email with the given `options`.
*
* Example Options:
*
* {
* from: "Fred Foo ✔ <foo@blurdybloop.com>", // sender address
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
* subject: "Hello ✔", // Subject line
* text: "Hello world ✔", // plaintext body
* html: "<b>Hello world ✔</b>", // html body
* transport: "gmail", // See 'alias' option above in setupTransport
* }
*
* See https://github.com/andris9/Nodemailer for other supported options.
*
* @param {Object} options
* @param {Function} callback Called after the e-mail is sent or the sending failed
*/
Mailer.send = function(options, fn) {
var dataSource = this.dataSource;
var settings = dataSource && dataSource.settings;
var connector = dataSource.connector;
assert(connector, 'Cannot send mail without a connector!');
var transport = connector.transportForName(options.transport);
if (!transport) {
transport = connector.defaultTransport();
}
if (debug.enabled || settings && settings.debug) {
g.log('Sending Mail:');
if (options.transport) {
console.log(g.f('\t TRANSPORT:%s', options.transport));
}
g.log('\t TO:%s', options.to);
g.log('\t FROM:%s', options.from);
g.log('\t SUBJECT:%s', options.subject);
g.log('\t TEXT:%s', options.text);
g.log('\t HTML:%s', options.html);
}
if (transport) {
assert(transport.sendMail,
'You must supply an Email.settings.transports containing a valid transport');
transport.sendMail(options, fn);
} else {
g.warn('Warning: No email transport specified for sending email.' +
' Setup a transport to send mail messages.');
process.nextTick(function() {
fn(null, options);
});
}
};
/**
* Send an email instance using `modelInstance.send()`.
*/
Mailer.prototype.send = function(fn) {
this.constructor.send(this, fn);
};
/**
* Access the node mailer object.
*/
MailConnector.mailer =
MailConnector.prototype.mailer =
Mailer.mailer =
Mailer.prototype.mailer = mailer;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 1 1 1 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2013,2014. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/**
* Expose `Memory`.
*/
'use strict';
module.exports = Memory;
/**
* Module dependencies.
*/
var Connector = require('./base-connector');
var debug = require('debug')('memory');
var util = require('util');
var inherits = util.inherits;
var assert = require('assert');
var JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory');
/**
* Create a new `Memory` connector with the given `options`.
*
* @param {Object} options
* @return {Memory}
*/
function Memory() {
// TODO implement entire memory connector
}
/**
* Inherit from `DBConnector`.
*/
inherits(Memory, Connector);
/**
* JugglingDB Compatibility
*/
Memory.initialize = JdbMemory.initialize;
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| context.js | 66.67% | (2 / 3) | 100% | (0 / 0) | 0% | (0 / 1) | 66.67% | (2 / 3) | |
| error-handler.js | 50% | (1 / 2) | 100% | (0 / 0) | 0% | (0 / 1) | 50% | (1 / 2) | |
| favicon.js | 60% | (3 / 5) | 0% | (0 / 2) | 0% | (0 / 1) | 60% | (3 / 5) | |
| rest.js | 20.83% | (5 / 24) | 0% | (0 / 12) | 0% | (0 / 4) | 20.83% | (5 / 24) | |
| static.js | 100% | (1 / 1) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (1 / 1) | |
| status.js | 40% | (2 / 5) | 100% | (0 / 0) | 0% | (0 / 2) | 40% | (2 / 5) | |
| token.js | 16.28% | (7 / 43) | 0% | (0 / 34) | 0% | (0 / 5) | 16.67% | (7 / 42) | |
| url-not-found.js | 33.33% | (2 / 6) | 100% | (0 / 0) | 0% | (0 / 2) | 33.33% | (2 / 6) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('../../lib/globalize');
module.exports = function() {
throw new Error(g.f(
'%s middleware was removed in version 3.0. See %s for more details.',
'loopback#context',
'http://loopback.io/doc/en/lb2/Using-current-context.html'));
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 | 1 | // Copyright IBM Corp. 2015. All Rights Reserved. // Node module: loopback // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; module.exports = function(options) { throw new Error('loopback.errorHandler is no longer available.' + ' Please use the module "strong-error-handler" instead.'); }; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 1 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var favicon = require('serve-favicon');
var path = require('path');
/**
* Serve the LoopBack favicon.
* @header loopback.favicon()
*/
module.exports = function(icon, options) {
icon = icon || path.join(__dirname, '../../favicon.ico');
return favicon(icon, options);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 1 1 1 1 1 | // Copyright IBM Corp. 2014,2015. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module dependencies.
*/
'use strict';
var g = require('../../lib/globalize');
var loopback = require('../../lib/loopback');
var async = require('async');
/*!
* Export the middleware.
*/
module.exports = rest;
/**
* Expose models over REST.
*
* For example:
* ```js
* app.use(loopback.rest());
* ```
* For more information, see [Exposing models over a REST API](http://loopback.io/doc/en/lb2/Exposing-models-over-REST.html).
* @header loopback.rest()
*/
function rest() {
var handlers; // Cached handlers
return function restApiHandler(req, res, next) {
var app = req.app;
var registry = app.registry;
if (!handlers) {
handlers = [];
var remotingOptions = app.get('remoting') || {};
var contextOptions = remotingOptions.context;
if (contextOptions !== undefined && contextOptions !== false) {
throw new Error(g.f(
'%s was removed in version 3.0. See %s for more details.',
'remoting.context option',
'http://loopback.io/doc/en/lb2/Using-current-context.html'));
}
if (app.isAuthEnabled) {
var AccessToken = registry.getModelByType('AccessToken');
handlers.push(loopback.token({model: AccessToken, app: app}));
}
handlers.push(function(req, res, next) {
// Need to get an instance of the REST handler per request
return app.handler('rest')(req, res, next);
});
}
if (handlers.length === 1) {
return handlers[0](req, res, next);
}
async.eachSeries(handlers, function(handler, done) {
handler(req, res, done);
}, next);
};
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 1 | // Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/**
* Serve static assets of a LoopBack application.
*
* @param {string} root The root directory from which the static assets are to
* be served.
* @param {object} options Refer to
* [express documentation](http://expressjs.com/4x/api.html#express.static)
* for the full list of available options.
* @header loopback.static(root, [options])
*/
'use strict';
module.exports = require('express').static;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Export the middleware.
*/
'use strict';
module.exports = status;
/**
* Return [HTTP response](http://expressjs.com/4x/api.html#res.send) with basic application status information:
* date the application was started and uptime, in JSON format.
* For example:
* ```js
* {
* "started": "2014-06-05T00:26:49.750Z",
* "uptime": 9.394
* }
* ```
*
* @header loopback.status()
*/
function status() {
var started = new Date();
return function(req, res) {
res.send({
started: started,
uptime: (Date.now() - Number(started)) / 1000,
});
};
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 1 1 1 1 1 1 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/*!
* Module dependencies.
*/
'use strict';
var loopback = require('../../lib/loopback');
var assert = require('assert');
var debug = require('debug')('loopback:middleware:token');
/*!
* Export the middleware.
*/
module.exports = token;
/*
* Rewrite the url to replace current user literal with the logged in user id
*/
function rewriteUserLiteral(req, currentUserLiteral) {
if (req.accessToken && req.accessToken.userId && currentUserLiteral) {
// Replace /me/ with /current-user-id/
var urlBeforeRewrite = req.url;
req.url = req.url.replace(
new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'),
'/' + req.accessToken.userId + '$1');
if (req.url !== urlBeforeRewrite) {
debug('req.url has been rewritten from %s to %s', urlBeforeRewrite,
req.url);
}
}
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Check for an access token in cookies, headers, and query string parameters.
* This function always checks for the following:
*
* - `access_token` (params only)
* - `X-Access-Token` (headers only)
* - `authorization` (headers and cookies)
*
* It checks for these values in cookies, headers, and query string parameters _in addition_ to the items
* specified in the options parameter.
*
* **NOTE:** This function only checks for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
*
* The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
* and header called `foo-auth`.
*
* ```js
* app.use(loopback.token({
* cookies: ['foo-auth'],
* headers: ['foo-auth', 'X-Foo-Auth'],
* params: ['foo-auth', 'foo_auth']
* }));
* ```
*
* @options {Object} [options] Each option array is used to add additional keys to find an `accessToken` for a `request`.
* @property {Array} [cookies] Array of cookie names.
* @property {Array} [headers] Array of header names.
* @property {Array} [params] Array of param names.
* @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request
* @property {Boolean} [enableDoublecheck] Execute middleware although an instance mounted earlier in the chain didn't find a token
* @property {Boolean} [overwriteExistingToken] only has effect in combination with `enableDoublecheck`. If truthy, will allow to overwrite an existing accessToken.
* @property {Function|String} [model] AccessToken model name or class to use.
* @property {String} [currentUserLiteral] String literal for the current user.
* @header loopback.token([options])
*/
function token(options) {
options = options || {};
var TokenModel;
var currentUserLiteral = options.currentUserLiteral;
if (currentUserLiteral && (typeof currentUserLiteral !== 'string')) {
debug('Set currentUserLiteral to \'me\' as the value is not a string.');
currentUserLiteral = 'me';
}
if (typeof currentUserLiteral === 'string') {
currentUserLiteral = escapeRegExp(currentUserLiteral);
}
var enableDoublecheck = !!options.enableDoublecheck;
var overwriteExistingToken = !!options.overwriteExistingToken;
return function(req, res, next) {
var app = req.app;
var registry = app.registry;
if (!TokenModel) {
TokenModel = registry.getModel(options.model || 'AccessToken');
}
assert(typeof TokenModel === 'function',
'loopback.token() middleware requires a AccessToken model');
if (req.accessToken !== undefined) {
if (!enableDoublecheck) {
// req.accessToken is defined already (might also be "null" or "false") and enableDoublecheck
// has not been set --> skip searching for credentials
rewriteUserLiteral(req, currentUserLiteral);
return next();
}
if (req.accessToken && req.accessToken.id && !overwriteExistingToken) {
// req.accessToken.id is defined, which means that some other middleware has identified a valid user.
// when overwriteExistingToken is not set to a truthy value, skip searching for credentials.
rewriteUserLiteral(req, currentUserLiteral);
return next();
}
// continue normal operation (as if req.accessToken was undefined)
}
TokenModel.findForRequest(req, options, function(err, token) {
req.accessToken = token || null;
rewriteUserLiteral(req, currentUserLiteral);
var ctx = req.loopbackContext;
if (ctx && ctx.active) ctx.set('accessToken', token);
next(err);
});
};
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 1 1 | // Copyright IBM Corp. 2014. All Rights Reserved. // Node module: loopback // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT /*! * Export the middleware. * See discussion in Connect pull request #954 for more details * https://github.com/senchalabs/connect/pull/954. */ 'use strict'; module.exports = urlNotFound; /** * Convert any request not handled so far to a 404 error * to be handled by error-handling middleware. * @header loopback.urlNotFound() */ function urlNotFound() { return function raiseUrlNotFoundError(req, res, next) { var error = new Error('Cannot ' + req.method + ' ' + req.url); error.status = 404; next(error); }; } |